tuber 0.0.1 → 0.5.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +68 -0
- data/.gitignore +17 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +112 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +27 -0
- data/README.md +379 -8
- data/Rakefile +35 -0
- data/TODO +1 -0
- data/examples/demo.rb +97 -0
- data/lib/tuber/configuration.rb +31 -0
- data/lib/tuber/connection.rb +366 -0
- data/lib/tuber/errors.rb +78 -0
- data/lib/tuber/job/collection.rb +160 -0
- data/lib/tuber/job/record.rb +226 -0
- data/lib/tuber/job.rb +4 -0
- data/lib/tuber/stats/fast_struct.rb +98 -0
- data/lib/tuber/stats/stat_struct.rb +52 -0
- data/lib/tuber/stats.rb +81 -0
- data/lib/tuber/tube/collection.rb +253 -0
- data/lib/tuber/tube/record.rb +231 -0
- data/lib/tuber/tube.rb +4 -0
- data/lib/tuber/version.rb +6 -0
- data/lib/tuber.rb +104 -2
- data/test/connection_test.rb +291 -0
- data/test/errors_test.rb +35 -0
- data/test/job_test.rb +250 -0
- data/test/jobs_test.rb +152 -0
- data/test/prompt_regexp_test.rb +59 -0
- data/test/stat_struct_test.rb +58 -0
- data/test/stats_test.rb +45 -0
- data/test/test_helper.rb +74 -0
- data/test/tube_test.rb +299 -0
- data/test/tuber_test.rb +112 -0
- data/test/tubes_test.rb +329 -0
- data/tuber.gemspec +33 -0
- metadata +112 -6
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tuber
|
|
4
|
+
# Exception to stop processing jobs during a `process!` loop.
|
|
5
|
+
# Simply `raise AbortProcessingError` in any job process handler to stop the processing loop.
|
|
6
|
+
class AbortProcessingError < RuntimeError; end
|
|
7
|
+
|
|
8
|
+
# Represents collection of job-related commands.
|
|
9
|
+
class Jobs
|
|
10
|
+
|
|
11
|
+
# @!attribute processors
|
|
12
|
+
# @return [Array<Proc>] returns Collection of proc to handle beanstalkd jobs
|
|
13
|
+
# @!attribute client
|
|
14
|
+
# @return [Tuber] returns the client instance
|
|
15
|
+
# @!attribute current_job
|
|
16
|
+
# @return [Tuber] returns the currently processing job in the process loop
|
|
17
|
+
attr_reader :processors, :client, :current_job
|
|
18
|
+
|
|
19
|
+
# Number of retries to process a job.
|
|
20
|
+
MAX_RETRIES = 3
|
|
21
|
+
|
|
22
|
+
# Delay in seconds before to make job ready again.
|
|
23
|
+
RELEASE_DELAY = 1
|
|
24
|
+
|
|
25
|
+
# Number of seconds to wait for a job before checking a different server.
|
|
26
|
+
RESERVE_TIMEOUT = nil
|
|
27
|
+
|
|
28
|
+
# Creates new jobs instance.
|
|
29
|
+
#
|
|
30
|
+
# @param [Tuber] client The tuber client instance.
|
|
31
|
+
# @example
|
|
32
|
+
# Tuber::Jobs.new(@client)
|
|
33
|
+
#
|
|
34
|
+
def initialize(client)
|
|
35
|
+
@client = client
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Delegates transmit to the connection object.
|
|
39
|
+
#
|
|
40
|
+
# @see Tuber::Connection#transmit
|
|
41
|
+
def transmit(command, options={})
|
|
42
|
+
# Empty **options must not be forwarded: on Ruby 2.7 it arrives as an
|
|
43
|
+
# extra positional {} at the receiver.
|
|
44
|
+
return client.connection.transmit(command) if options.empty?
|
|
45
|
+
client.connection.transmit(command, **options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Peek (or find) job by id from beanstalkd.
|
|
49
|
+
#
|
|
50
|
+
# @param [Integer] id Job id to find
|
|
51
|
+
# @return [Tuber::Job] Job matching given id
|
|
52
|
+
# @example
|
|
53
|
+
# @tuber.jobs[123] # => <Tuber::Job>
|
|
54
|
+
# @tuber.jobs.find(123) # => <Tuber::Job>
|
|
55
|
+
# @tuber.jobs.peek(123) # => <Tuber::Job>
|
|
56
|
+
#
|
|
57
|
+
# @api public
|
|
58
|
+
def find(id)
|
|
59
|
+
res = transmit("peek #{id}")
|
|
60
|
+
Job.new(client, res)
|
|
61
|
+
rescue Tuber::NotFoundError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
alias_method :peek, :find
|
|
65
|
+
alias_method :[], :find
|
|
66
|
+
|
|
67
|
+
# Extends the ttr of every job currently reserved by this connection.
|
|
68
|
+
#
|
|
69
|
+
# A single heartbeat for a whole +reserve_batch+ window: the server already
|
|
70
|
+
# tracks the reserved set per connection, so no ids are sent and jobs that
|
|
71
|
+
# were already deleted, released, buried or lost to a ttr timeout are simply
|
|
72
|
+
# absent. Each job keeps its own ttr; deadlines are extended individually.
|
|
73
|
+
#
|
|
74
|
+
# The returned count is how many jobs the connection *actually* still holds.
|
|
75
|
+
# If it is lower than expected, jobs hit their ttr and went back to the queue
|
|
76
|
+
# while the worker was busy.
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer] Number of held jobs whose deadline was extended
|
|
79
|
+
# @example
|
|
80
|
+
# @tuber.jobs.touch_all # => 10
|
|
81
|
+
#
|
|
82
|
+
# @api public
|
|
83
|
+
def touch_all
|
|
84
|
+
transmit("touch-all")[:id].to_i
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Register a processor to handle beanstalkd job on particular tube.
|
|
88
|
+
#
|
|
89
|
+
# @param [String] tube_name Tube name
|
|
90
|
+
# @param [Hash{String=>RuntimeError}] options settings for processor
|
|
91
|
+
# @param [Proc] block Process beanstalkd job
|
|
92
|
+
# @option options [Integer] max_retries Number of retries to process a job
|
|
93
|
+
# @option options [Array<RuntimeError>] retry_on Collection of errors to rescue and re-run processor
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# @beanstalk.jobs.register('some-tube', :retry_on => [SomeError]) do |job|
|
|
97
|
+
# do_something(job)
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# @beanstalk.jobs.register('other-tube') do |job|
|
|
101
|
+
# do_something_else(job)
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# @api public
|
|
105
|
+
def register(tube_name, options={}, &block)
|
|
106
|
+
@processors ||= {}
|
|
107
|
+
max_retries = options[:max_retries] || MAX_RETRIES
|
|
108
|
+
retry_on = Array(options[:retry_on])
|
|
109
|
+
@processors[tube_name.to_s] = { :block => block, :retry_on => retry_on, :max_retries => max_retries }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Sets flag to indicate that process loop should stop after current job
|
|
113
|
+
def stop!
|
|
114
|
+
@stop = true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns whether the process loop should stop
|
|
118
|
+
#
|
|
119
|
+
# @return [Boolean] if true the loop should stop after current processing
|
|
120
|
+
def stop?
|
|
121
|
+
!!@stop
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Watch, reserve, process and delete or bury or release jobs.
|
|
125
|
+
#
|
|
126
|
+
# @param [Hash{String => Integer}] options Settings for processing
|
|
127
|
+
# @option options [Integer] release_delay Delay in seconds before to make job ready again
|
|
128
|
+
# @option options [Integer] reserve_timeout Number of seconds to wait for a job before checking a different server
|
|
129
|
+
#
|
|
130
|
+
# @api public
|
|
131
|
+
def process!(options={})
|
|
132
|
+
release_delay = options.delete(:release_delay) || RELEASE_DELAY
|
|
133
|
+
reserve_timeout = options.delete(:reserve_timeout) || RESERVE_TIMEOUT
|
|
134
|
+
client.tubes.watch!(*processors.keys)
|
|
135
|
+
while !stop? do
|
|
136
|
+
begin
|
|
137
|
+
@current_job = client.tubes.reserve(reserve_timeout)
|
|
138
|
+
processor = processors[@current_job.tube]
|
|
139
|
+
begin
|
|
140
|
+
processor[:block].call(@current_job)
|
|
141
|
+
@current_job.delete
|
|
142
|
+
rescue *processor[:retry_on]
|
|
143
|
+
if @current_job.stats.releases < processor[:max_retries]
|
|
144
|
+
@current_job.release(:delay => release_delay)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
rescue AbortProcessingError
|
|
148
|
+
break
|
|
149
|
+
rescue Tuber::JobNotReserved, Tuber::NotFoundError, Tuber::TimedOutError
|
|
150
|
+
retry
|
|
151
|
+
rescue StandardError # handles unspecified errors
|
|
152
|
+
@current_job.bury if @current_job
|
|
153
|
+
ensure # bury if still reserved
|
|
154
|
+
@current_job.bury if @current_job && @current_job.exists? && @current_job.reserved?
|
|
155
|
+
@current_job = nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end # process!
|
|
159
|
+
end # Jobs
|
|
160
|
+
end # Tuber
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tuber
|
|
4
|
+
# Represents job related commands.
|
|
5
|
+
class Job
|
|
6
|
+
|
|
7
|
+
# @!attribute id
|
|
8
|
+
# @return [Integer] id for the job.
|
|
9
|
+
# @!attribute body
|
|
10
|
+
# @return [String] the job's body.
|
|
11
|
+
# @!attribute reserved
|
|
12
|
+
# @return [Boolean] whether the job has been reserved.
|
|
13
|
+
# @!attribute client
|
|
14
|
+
# @return [Tuber] returns the client instance
|
|
15
|
+
attr_reader :id, :body, :reserved, :client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Initializes a new job object.
|
|
19
|
+
#
|
|
20
|
+
# @param [Hash{Symbol => String,Number}] res Result from beanstalkd response
|
|
21
|
+
#
|
|
22
|
+
def initialize(client, res)
|
|
23
|
+
@client = client
|
|
24
|
+
@id = res[:id]
|
|
25
|
+
@body = res[:body]
|
|
26
|
+
@reserved = res[:status] == 'RESERVED'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sends command to bury a reserved job.
|
|
30
|
+
#
|
|
31
|
+
# @param [Hash{Symbol => Integer}] options Settings to bury job
|
|
32
|
+
# @option options [Integer] pri Assign new priority to job
|
|
33
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# @job.bury({:pri => 100})
|
|
37
|
+
# # => {:status=>"BURIED", :body=>nil}
|
|
38
|
+
#
|
|
39
|
+
# @api public
|
|
40
|
+
def bury(options={})
|
|
41
|
+
options = { :pri => stats.pri }.merge(options)
|
|
42
|
+
with_reserved("bury #{id} #{options[:pri]}") do
|
|
43
|
+
@reserved = false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sends command to release a job back to ready state.
|
|
48
|
+
#
|
|
49
|
+
# @param [Hash{String => Integer}] options Settings to release job
|
|
50
|
+
# @option options [Integer] pri Assign new priority to job
|
|
51
|
+
# @option options [Integer] delay Assign new delay to job
|
|
52
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
53
|
+
# @example
|
|
54
|
+
# @tuber.jobs.find(123).release(:pri => 10, :delay => 5)
|
|
55
|
+
# # => {:status=>"RELEASED", :body=>nil}
|
|
56
|
+
#
|
|
57
|
+
# @api public
|
|
58
|
+
def release(options={})
|
|
59
|
+
options = { :pri => stats.pri, :delay => stats.delay }.merge(options)
|
|
60
|
+
with_reserved("release #{id} #{options[:pri]} #{options[:delay]}") do
|
|
61
|
+
@reserved = false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Sends command to touch job which extends the ttr.
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
68
|
+
# @example
|
|
69
|
+
# @tuber.jobs.find(123).touch
|
|
70
|
+
# # => {:status=>"TOUCHED", :body=>nil}
|
|
71
|
+
#
|
|
72
|
+
# @api public
|
|
73
|
+
def touch
|
|
74
|
+
with_reserved("touch #{id}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Sends command to delete a job.
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
80
|
+
# @example
|
|
81
|
+
# @tuber.jobs.find(123).delete
|
|
82
|
+
# # => {:status=>"DELETED", :body=>nil}
|
|
83
|
+
#
|
|
84
|
+
# @api public
|
|
85
|
+
def delete
|
|
86
|
+
transmit("delete #{id}") { @reserved = false }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Sends command to kick a buried job.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
92
|
+
# @example
|
|
93
|
+
# @tuber.jobs.find(123).kick
|
|
94
|
+
# # => {:status=>"KICKED", :body=>nil}
|
|
95
|
+
#
|
|
96
|
+
# @api public
|
|
97
|
+
def kick
|
|
98
|
+
transmit("kick-job #{id}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Sends command to get stats about job.
|
|
102
|
+
#
|
|
103
|
+
# @return [Tuber::StatStruct] struct filled with relevant job stats
|
|
104
|
+
# @example
|
|
105
|
+
# @tuber.jobs.find(123).stats
|
|
106
|
+
# @job.stats.tube # => "some-tube"
|
|
107
|
+
#
|
|
108
|
+
# @api public
|
|
109
|
+
def stats
|
|
110
|
+
res = transmit("stats-job #{id}")
|
|
111
|
+
StatStruct.from_hash(res[:body])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if job is currently in a reserved state.
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] Returns true if the job is in a reserved state
|
|
117
|
+
# @example
|
|
118
|
+
# @tuber.jobs.find(123).reserved?
|
|
119
|
+
#
|
|
120
|
+
# @api public
|
|
121
|
+
def reserved?
|
|
122
|
+
@reserved || self.stats.state == "reserved"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check if the job still exists.
|
|
126
|
+
#
|
|
127
|
+
# @return [Boolean] Returns true if the job still exists
|
|
128
|
+
# @example
|
|
129
|
+
# @tuber.jobs.find(123).exists?
|
|
130
|
+
#
|
|
131
|
+
# @api public
|
|
132
|
+
def exists?
|
|
133
|
+
!self.stats.nil?
|
|
134
|
+
rescue Tuber::NotFoundError
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns the name of the tube this job is in
|
|
139
|
+
#
|
|
140
|
+
# @return [String] The name of the tube for this job
|
|
141
|
+
# @example
|
|
142
|
+
# @tuber.jobs.find(123).tube
|
|
143
|
+
# # => "some-tube"
|
|
144
|
+
#
|
|
145
|
+
# @api public
|
|
146
|
+
def tube
|
|
147
|
+
@tube ||= self.stats.tube
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns the ttr of this job
|
|
151
|
+
#
|
|
152
|
+
# @return [Integer] The ttr of this job
|
|
153
|
+
# @example
|
|
154
|
+
# @tuber.jobs.find(123).ttr
|
|
155
|
+
# # => 123
|
|
156
|
+
#
|
|
157
|
+
# @api public
|
|
158
|
+
def ttr
|
|
159
|
+
@ttr ||= self.stats.ttr
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns the pri of this job
|
|
163
|
+
#
|
|
164
|
+
# @return [Integer] The pri of this job
|
|
165
|
+
# @example
|
|
166
|
+
# @tuber.jobs.find(123).pri
|
|
167
|
+
# # => 1
|
|
168
|
+
#
|
|
169
|
+
def pri
|
|
170
|
+
self.stats.pri
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns the delay of this job
|
|
174
|
+
#
|
|
175
|
+
# @return [Integer] The delay of this job
|
|
176
|
+
# @example
|
|
177
|
+
# @tuber.jobs.find(123).delay
|
|
178
|
+
# # => 5
|
|
179
|
+
#
|
|
180
|
+
def delay
|
|
181
|
+
self.stats.delay
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns string representation of job
|
|
185
|
+
#
|
|
186
|
+
# @return [String] string representation
|
|
187
|
+
# @example
|
|
188
|
+
# @tuber.jobs.find(123).to_s
|
|
189
|
+
# @tuber.jobs.find(123).inspect
|
|
190
|
+
#
|
|
191
|
+
def to_s
|
|
192
|
+
"#<Tuber::Job id=#{id} body=#{body.inspect}>"
|
|
193
|
+
end
|
|
194
|
+
alias :inspect :to_s
|
|
195
|
+
|
|
196
|
+
protected
|
|
197
|
+
|
|
198
|
+
# Transmit command to beanstalkd instance and fetch response.
|
|
199
|
+
#
|
|
200
|
+
# @param [String] cmd Beanstalkd command to send.
|
|
201
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
202
|
+
# @example
|
|
203
|
+
# transmit('stats')
|
|
204
|
+
# transmit('stats') { 'success' }
|
|
205
|
+
#
|
|
206
|
+
def transmit(cmd, &block)
|
|
207
|
+
res = client.connection.transmit(cmd)
|
|
208
|
+
yield if block_given?
|
|
209
|
+
res
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Transmits a command which requires the job to be reserved.
|
|
213
|
+
#
|
|
214
|
+
# @param [String] cmd Beanstalkd command to send.
|
|
215
|
+
# @return [Hash{Symbol => String,Number}] Beanstalkd response for the command.
|
|
216
|
+
# @raise [Tuber::JobNotReserved] Command cannot execute since job is not reserved.
|
|
217
|
+
# @example
|
|
218
|
+
# with_reserved("bury 26") { @reserved = false }
|
|
219
|
+
#
|
|
220
|
+
def with_reserved(cmd, &block)
|
|
221
|
+
raise JobNotReserved unless reserved?
|
|
222
|
+
transmit(cmd, &block)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
end # Job
|
|
226
|
+
end # Tuber
|
data/lib/tuber/job.rb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tuber
|
|
4
|
+
#
|
|
5
|
+
# Borrowed from:
|
|
6
|
+
# https://github.com/dolzenko/faster_open_struct/blob/master/lib/faster_open_struct.rb
|
|
7
|
+
#
|
|
8
|
+
# Up to 40 (!) times more memory efficient version of OpenStruct
|
|
9
|
+
#
|
|
10
|
+
# Differences from Ruby MRI OpenStruct:
|
|
11
|
+
#
|
|
12
|
+
# 1. Doesn't `dup` passed initialization hash (NOTE: only reference to hash is stored)
|
|
13
|
+
#
|
|
14
|
+
# 2. Doesn't convert hash keys to symbols (by default string keys are used,
|
|
15
|
+
# with fallback to symbol keys)
|
|
16
|
+
#
|
|
17
|
+
# 3. Creates methods on the fly on `OpenStruct` class, instead of singleton class.
|
|
18
|
+
# Uses `module_eval` with string to avoid holding scope references for every method.
|
|
19
|
+
#
|
|
20
|
+
# 4. Refactored, crud clean, spec covered :)
|
|
21
|
+
#
|
|
22
|
+
# @private
|
|
23
|
+
class FasterOpenStruct
|
|
24
|
+
# Undefine particularly nasty interfering methods on Ruby 1.8
|
|
25
|
+
undef :type if method_defined?(:type)
|
|
26
|
+
undef :id if method_defined?(:id)
|
|
27
|
+
|
|
28
|
+
def initialize(hash = nil)
|
|
29
|
+
@hash = hash || {}
|
|
30
|
+
@initialized_empty = hash == nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def method_missing(method_name_sym, *args)
|
|
34
|
+
if method_name_sym.to_s[-1] == ?=
|
|
35
|
+
if args.size != 1
|
|
36
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)", caller(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if self.frozen?
|
|
40
|
+
raise TypeError, "can't modify frozen #{self.class}", caller(1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
__new_ostruct_member__(method_name_sym.to_s.chomp("="))
|
|
44
|
+
send(method_name_sym, args[0])
|
|
45
|
+
elsif args.size == 0
|
|
46
|
+
__new_ostruct_member__(method_name_sym)
|
|
47
|
+
send(method_name_sym)
|
|
48
|
+
else
|
|
49
|
+
raise NoMethodError, "undefined method `#{method_name_sym}' for #{self}", caller(1)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def __new_ostruct_member__(method_name_sym)
|
|
54
|
+
self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
55
|
+
def #{ method_name_sym }
|
|
56
|
+
@hash.fetch("#{ method_name_sym }", @hash[:#{ method_name_sym }]) # read by default from string key, then try symbol
|
|
57
|
+
# if string key doesn't exist
|
|
58
|
+
end
|
|
59
|
+
END_EVAL
|
|
60
|
+
|
|
61
|
+
unless method_name_sym.to_s[-1] == ?? # can't define writer for predicate method
|
|
62
|
+
self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
63
|
+
def #{ method_name_sym }=(val)
|
|
64
|
+
if @hash.key?("#{ method_name_sym }") || @initialized_empty # write by default to string key (when it is present
|
|
65
|
+
# in initialization hash or initialization hash
|
|
66
|
+
# wasn't provided)
|
|
67
|
+
@hash["#{ method_name_sym }"] = val # if it doesn't exist - write to symbol key
|
|
68
|
+
else
|
|
69
|
+
@hash[:#{ method_name_sym }] = val
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
END_EVAL
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def empty?
|
|
77
|
+
@hash.empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Compare this object and +other+ for equality.
|
|
82
|
+
#
|
|
83
|
+
def ==(other)
|
|
84
|
+
return false unless other.is_a?(self.class)
|
|
85
|
+
@hash == other.instance_variable_get(:@hash)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#
|
|
89
|
+
# Returns a string containing a detailed summary of the keys and values.
|
|
90
|
+
#
|
|
91
|
+
def inspect
|
|
92
|
+
str = "#<#{ self.class }"
|
|
93
|
+
str << " #{ @hash.map { |k, v| "#{ k }=#{ v.inspect }" }.join(", ") }" unless @hash.empty?
|
|
94
|
+
str << ">"
|
|
95
|
+
end
|
|
96
|
+
alias :to_s :inspect
|
|
97
|
+
end # FasterOpenStruct
|
|
98
|
+
end # Tuber
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tuber
|
|
4
|
+
# Represents a stats hash with proper underscored keys
|
|
5
|
+
class StatStruct < FasterOpenStruct
|
|
6
|
+
# Convert a stats hash into a struct.
|
|
7
|
+
#
|
|
8
|
+
# @param [Hash{String => String}] hash Hash Stats hash to convert to struct
|
|
9
|
+
# @return [Tuber::StatStruct, nil] Stats struct from hash
|
|
10
|
+
# @example
|
|
11
|
+
# s = StatStruct.from_hash(:foo => "bar")
|
|
12
|
+
# s.foo # => 'bar'
|
|
13
|
+
#
|
|
14
|
+
def self.from_hash(hash)
|
|
15
|
+
return unless hash.is_a?(Hash)
|
|
16
|
+
underscore_hash = hash.inject({}) { |r, (k, v)| r[k.to_s.gsub(/-/, '_')] = v; r }
|
|
17
|
+
self.new(underscore_hash)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Access value for stat with specified key.
|
|
21
|
+
#
|
|
22
|
+
# Keys are stored underscored (see {.from_hash}), so hyphenated beanstalkd
|
|
23
|
+
# names like "current-jobs-ready" are underscored before lookup. This also
|
|
24
|
+
# avoids defining an invalid, hyphenated method name via method_missing.
|
|
25
|
+
#
|
|
26
|
+
# @param [String] key Key to fetch from stats.
|
|
27
|
+
# @return [String, Integer] Value for specified stat key.
|
|
28
|
+
# @example
|
|
29
|
+
# @stats['foo'] # => "bar"
|
|
30
|
+
# @stats['current-jobs-ready'] # => 5
|
|
31
|
+
#
|
|
32
|
+
def [](key)
|
|
33
|
+
self.send(key.to_s.gsub(/-/, '_'))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns set of keys within this struct
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<String>] Value for specified stat key.
|
|
39
|
+
# @example
|
|
40
|
+
# @stats.keys # => ['foo', 'bar', 'baz']
|
|
41
|
+
#
|
|
42
|
+
def keys
|
|
43
|
+
@hash.keys.map { |k| k.to_s }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the initialization hash
|
|
47
|
+
#
|
|
48
|
+
def to_h
|
|
49
|
+
@hash
|
|
50
|
+
end
|
|
51
|
+
end # StatStruct
|
|
52
|
+
end # Tuber
|
data/lib/tuber/stats.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tuber/stats/fast_struct'
|
|
4
|
+
require 'tuber/stats/stat_struct'
|
|
5
|
+
|
|
6
|
+
class Tuber
|
|
7
|
+
# Represents stats related to the beanstalkd pool.
|
|
8
|
+
class Stats
|
|
9
|
+
|
|
10
|
+
# @!attribute client
|
|
11
|
+
# @return [Tuber] returns the client instance
|
|
12
|
+
attr_reader :client
|
|
13
|
+
|
|
14
|
+
# Creates new stats instance.
|
|
15
|
+
#
|
|
16
|
+
# @param [Tuber] client The tuber client instance.
|
|
17
|
+
# @example
|
|
18
|
+
# Tuber::Stats.new(@client)
|
|
19
|
+
#
|
|
20
|
+
def initialize(client)
|
|
21
|
+
@client = client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns keys for stats data
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<String>] Set of keys for stats.
|
|
27
|
+
# @example
|
|
28
|
+
# @bp.stats.keys # => ["version", "total_connections"]
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
def keys
|
|
32
|
+
data.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns value for specified key.
|
|
36
|
+
#
|
|
37
|
+
# @param [String,Symbol] key Name of key to retrieve
|
|
38
|
+
# @return [String,Integer] Value of specified key
|
|
39
|
+
# @example
|
|
40
|
+
# @bp.stats['total_connections'] # => 4
|
|
41
|
+
#
|
|
42
|
+
def [](key)
|
|
43
|
+
data[key]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Delegates inspection to the real data structure
|
|
47
|
+
#
|
|
48
|
+
# @return [String] returns a string containing a detailed stats summary
|
|
49
|
+
def inspect
|
|
50
|
+
data.to_s
|
|
51
|
+
end
|
|
52
|
+
alias :to_s :inspect
|
|
53
|
+
|
|
54
|
+
# Defines a cached method for looking up data for specified key
|
|
55
|
+
# Protects against infinite loops by checking stacktrace
|
|
56
|
+
# @api public
|
|
57
|
+
def method_missing(name, *args, &block)
|
|
58
|
+
if caller.first !~ /`(method_missing|data')/ && data.keys.include?(name.to_s)
|
|
59
|
+
self.class.class_eval <<-CODE, __FILE__, __LINE__
|
|
60
|
+
def #{name}; data[#{name.inspect}]; end
|
|
61
|
+
CODE
|
|
62
|
+
data[name.to_s]
|
|
63
|
+
else # no key matches or caught infinite loop
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
protected
|
|
69
|
+
|
|
70
|
+
# Returns struct based on stats data from response.
|
|
71
|
+
#
|
|
72
|
+
# @return [Tuber::StatStruct] the stats
|
|
73
|
+
# @example
|
|
74
|
+
# self.data # => { 'version' : 1.7, 'total_connections' : 23 }
|
|
75
|
+
# self.data.total_connections # => 23
|
|
76
|
+
#
|
|
77
|
+
def data
|
|
78
|
+
StatStruct.from_hash(client.connection.transmit('stats')[:body])
|
|
79
|
+
end
|
|
80
|
+
end # Stats
|
|
81
|
+
end # Tuber
|