tuber 0.0.1 → 0.6.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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tuber
4
+ # Represents collection of tube related commands.
5
+
6
+ class Tubes
7
+ include Enumerable
8
+
9
+ # @!attribute client
10
+ # @return [Tuber] returns the client instance
11
+ attr_reader :client
12
+
13
+ # Creates new tubes instance.
14
+ #
15
+ # @param [Tuber] client The tuber client instance.
16
+ # @example
17
+ # Tuber::Tubes.new(@client)
18
+ #
19
+ def initialize(client)
20
+ @client = client
21
+ end
22
+
23
+ def last_used
24
+ client.connection.tube_used
25
+ end
26
+
27
+ def last_used=(tube_name)
28
+ client.connection.tube_used = tube_name
29
+ end
30
+
31
+ # Delegates transmit to the connection object.
32
+ #
33
+ # @see Tuber::Connection#transmit
34
+ def transmit(command, **options)
35
+ # Empty **options must not be forwarded: on Ruby 2.7 it arrives as an
36
+ # extra positional {} at the receiver.
37
+ return client.connection.transmit(command) if options.empty?
38
+ client.connection.transmit(command, **options)
39
+ end
40
+
41
+ # Finds the specified beanstalk tube.
42
+ #
43
+ # @param [String] tube_name Name of the beanstalkd tube
44
+ # @return [Tuber::Tube] specified tube
45
+ # @example
46
+ # @pool.tubes.find('tube2')
47
+ # @pool.tubes['tube2']
48
+ # # => <Tuber::Tube name="tube2">
49
+ #
50
+ # @api public
51
+ def find(tube_name)
52
+ Tube.new(client, tube_name)
53
+ end
54
+ alias_method :[], :find
55
+
56
+ # Reserves a ready job looking at all watched tubes.
57
+ #
58
+ # @param [Integer] timeout Number of seconds before timing out.
59
+ # @param [Proc] block Callback to perform on the reserved job.
60
+ # @yield [job] Reserved tuber job.
61
+ # @return [Tuber::Job] Reserved tuber job.
62
+ # @example
63
+ # @client.tubes.reserve { |job| process(job) }
64
+ # # => <Tuber::Job id=5 body="foo">
65
+ #
66
+ # @api public
67
+ def reserve(timeout=nil, &block)
68
+ res = transmit(
69
+ timeout ? "reserve-with-timeout #{timeout}" : 'reserve')
70
+ job = Job.new(client, res)
71
+ block.call(job) if block_given?
72
+ job
73
+ end
74
+
75
+ # Reserves a batch of ready jobs from watched tubes.
76
+ #
77
+ # Without a +timeout+ the call is non-blocking and may return an empty array.
78
+ # With a positive +timeout+ it long-polls, blocking up to +timeout+ seconds
79
+ # for the first job before draining whatever is ready, up to +count+.
80
+ #
81
+ # @param [Integer] count Maximum number of jobs to reserve
82
+ # @param [Integer] timeout Seconds to long-poll for the first job (nil = non-blocking)
83
+ # @return [Array<Tuber::Job>] Array of reserved jobs (empty if none available)
84
+ # @raise [Tuber::DeadlineSoonError] A reserved job's TTR is about to expire
85
+ # @example
86
+ # @client.tubes.reserve_batch(10) # non-blocking
87
+ # @client.tubes.reserve_batch(10, 30) # long-poll up to 30s
88
+ # # => [<Tuber::Job id=1 body="foo">, ...]
89
+ #
90
+ # @api public
91
+ def reserve_batch(count, timeout = nil)
92
+ results = client.connection.reserve_batch(count, timeout)
93
+ results.map { |res| Job.new(client, res) }
94
+ end
95
+
96
+ # Sets the reserve mode for the connection.
97
+ #
98
+ # @param [String, Symbol] mode The reserve mode ('weighted' or 'fifo')
99
+ # @return [Hash] Response from beanstalkd
100
+ # @example
101
+ # @client.tubes.reserve_mode(:weighted)
102
+ # @client.tubes.reserve_mode(:fifo)
103
+ #
104
+ # @api public
105
+ def reserve_mode(mode)
106
+ res = transmit("reserve-mode #{mode}")
107
+ # Remembered so a reconnect can replay it: reserve mode is per-connection
108
+ # server state, and a fresh socket starts back at fifo.
109
+ client.connection.reserve_mode = mode
110
+ res
111
+ end
112
+
113
+ # Reserves a specific job by its ID.
114
+ #
115
+ # @param [Integer, String] id The job ID to reserve
116
+ # @return [Tuber::Job, nil] The reserved job, or nil if not found
117
+ # @example
118
+ # @client.tubes.reserve_job(123)
119
+ # # => <Tuber::Job id=123 body="foo">
120
+ #
121
+ # @api public
122
+ def reserve_job(id)
123
+ res = transmit("reserve-job #{id}")
124
+ Job.new(client, res)
125
+ rescue Tuber::NotFoundError
126
+ nil
127
+ end
128
+
129
+ # Returns stats for a job group.
130
+ #
131
+ # @param [String] group The group name
132
+ # @return [Tuber::StatStruct] Struct of group stats
133
+ # @example
134
+ # @client.tubes.stats_group('batch-1')
135
+ # # => #<StatStruct name="batch-1" ready=5 reserved=0 delayed=0 buried=0 waiting_jobs=0>
136
+ #
137
+ # @api public
138
+ def stats_group(group)
139
+ res = transmit("stats-group #{group}")
140
+ StatStruct.from_hash(res[:body])
141
+ end
142
+
143
+ # List of all known beanstalk tubes.
144
+ #
145
+ # @return [Array<Tuber::Tube>] List of all beanstalk tubes.
146
+ # @example
147
+ # @client.tubes.all
148
+ # # => [<Tuber::Tube name="tube2">, <Tuber::Tube name="tube3">]
149
+ #
150
+ # @api public
151
+ def all
152
+ transmit('list-tubes')[:body].map do |tube_name|
153
+ Tube.new(client, tube_name)
154
+ end
155
+ end
156
+
157
+ # Calls the given block once for each known beanstalk tube, passing that element as a parameter.
158
+ #
159
+ # @return An Enumerator is returned if no block is given.
160
+ # @example
161
+ # @pool.tubes.each {|t| puts t.name}
162
+ #
163
+ # @api public
164
+ def each(&block)
165
+ all.each(&block)
166
+ end
167
+
168
+ # List of watched beanstalk tubes.
169
+ #
170
+ # @return [Array<Tuber::Tube>] List of watched beanstalk tubes.
171
+ # @example
172
+ # @client.tubes.watched
173
+ # # => [<Tuber::Tube name="tube2">, <Tuber::Tube name="tube3">]
174
+ #
175
+ # @api public
176
+ def watched
177
+ last_watched = transmit('list-tubes-watched')[:body]
178
+ client.connection.tubes_watched = last_watched.dup
179
+ last_watched.map do |tube_name|
180
+ Tube.new(client, tube_name)
181
+ end
182
+ end
183
+
184
+ # Currently used beanstalk tube.
185
+ #
186
+ # @return [Tuber::Tube] Currently used beanstalk tube.
187
+ # @example
188
+ # @client.tubes.used
189
+ # # => <Tuber::Tube name="tube2">
190
+ #
191
+ # @api public
192
+ def used
193
+ last_used = transmit('list-tube-used')[:id]
194
+ Tube.new(client, last_used)
195
+ end
196
+
197
+ # Add specified beanstalkd tubes as watched.
198
+ #
199
+ # @param [*String] names Name of tubes to watch
200
+ # @raise [Tuber::InvalidTubeName] Tube to watch was invalid.
201
+ # @example
202
+ # @client.tubes.watch('foo', 'bar')
203
+ #
204
+ # @api public
205
+ def watch(*names, weight: nil)
206
+ names.each do |t|
207
+ cmd = weight ? "watch #{t} #{weight}" : "watch #{t}"
208
+ transmit cmd
209
+ client.connection.add_to_watched(t, weight)
210
+ end
211
+ rescue BadFormatError => ex
212
+ raise InvalidTubeName, "Tube in '#{ex.cmd}' is invalid!"
213
+ end
214
+
215
+ # Add specified beanstalkd tubes as watched and ignores all other tubes.
216
+ #
217
+ # @param [*String] names Name of tubes to watch
218
+ # @raise [Tuber::InvalidTubeName] Tube to watch was invalid.
219
+ # @example
220
+ # @client.tubes.watch!('foo', 'bar')
221
+ #
222
+ # @api public
223
+ def watch!(*names)
224
+ old_tubes = watched.map(&:name) - names.map(&:to_s)
225
+ watch(*names)
226
+ ignore(*old_tubes)
227
+ end
228
+
229
+ # Ignores specified beanstalkd tubes.
230
+ #
231
+ # @param [*String] names Name of tubes to ignore
232
+ # @example
233
+ # @client.tubes.ignore('foo', 'bar')
234
+ #
235
+ # @api public
236
+ def ignore(*names)
237
+ names.each do |w|
238
+ transmit "ignore #{w}"
239
+ client.connection.remove_from_watched(w)
240
+ end
241
+ end
242
+
243
+ # Set specified tube as used.
244
+ #
245
+ # @param [String] tube Tube to be used.
246
+ # @example
247
+ # @conn.tubes.use("some-tube")
248
+ #
249
+ def use(tube)
250
+ return tube if last_used == tube
251
+ transmit("use #{tube}")
252
+ self.last_used = tube
253
+ rescue BadFormatError
254
+ raise InvalidTubeName, "Tube cannot be named '#{tube}'"
255
+ end
256
+ end # Tubes
257
+ end # Tuber
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tuber
4
+ # Beanstalk tube which contains jobs which can be inserted, reserved, et al.
5
+ class Tube
6
+
7
+ # @!attribute name
8
+ # @return [String] name of the tube
9
+ # @!attribute client
10
+ # @return [Tuber] returns the client instance
11
+ attr_reader :name, :client
12
+
13
+ # Fetches the specified tube.
14
+ #
15
+ # @param [Tuber] client The tuber client instance.
16
+ # @param [String] name The name for this tube.
17
+ # @example
18
+ # Tuber::Tube.new(@client, 'tube-name')
19
+ #
20
+ def initialize(client, name)
21
+ @client = client
22
+ @name = name.to_s
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ # Delegates transmit to the connection object.
27
+ #
28
+ # @see Tuber::Connection#transmit
29
+ def transmit(command, options={})
30
+ # Empty **options must not be forwarded: on Ruby 2.7 it arrives as an
31
+ # extra positional {} at the receiver.
32
+ return client.connection.transmit(command) if options.empty?
33
+ client.connection.transmit(command, **options)
34
+ end
35
+
36
+ # Inserts job with specified body onto tube.
37
+ #
38
+ # @param [String] body The data to store with this job.
39
+ # @param [Hash{String => Integer}] options The settings associated with this job.
40
+ # @option options [Integer] pri priority for this job
41
+ # @option options [Integer] ttr time to respond for this job
42
+ # @option options [Integer] delay delay for this job
43
+ # @option options [String, Array] idp idempotency key, or [key, ttl] pair
44
+ # @option options [String, Array] con concurrency key, or [key, limit] pair
45
+ # @option options [String] grp group key for this job
46
+ # @option options [String] aft after-group key (run after group completes)
47
+ # @return [Hash{String => String, Number}] beanstalkd command response
48
+ # @example
49
+ # @tube.put "data", pri: 1000, ttr: 10, delay: 5
50
+ # @tube.put "data", idp: "report", con: ["db", 3], grp: "batch-1", aft: "batch-0"
51
+ #
52
+ # @api public
53
+ def put(body, options={})
54
+ safe_use do
55
+ serialized_body = config.job_serializer.call(body)
56
+
57
+ options = {
58
+ :pri => config.default_put_pri,
59
+ :delay => config.default_put_delay,
60
+ :ttr => config.default_put_ttr
61
+ }.merge(options)
62
+
63
+ cmd_options = "#{options[:pri]} #{options[:delay]} #{options[:ttr]} #{serialized_body.bytesize}"
64
+
65
+ tags = []
66
+ if options[:idp]
67
+ tags << (options[:idp].is_a?(Array) ? "idp:#{options[:idp].join(':')}" : "idp:#{options[:idp]}")
68
+ end
69
+ if options[:con]
70
+ tags << (options[:con].is_a?(Array) ? "con:#{options[:con].join(':')}" : "con:#{options[:con]}")
71
+ end
72
+ tags << "grp:#{options[:grp]}" if options[:grp]
73
+ tags << "aft:#{options[:aft]}" if options[:aft]
74
+
75
+ cmd_options = "#{cmd_options} #{tags.join(' ')}" if tags.any?
76
+ transmit("put #{cmd_options}\r\n#{serialized_body}")
77
+ end
78
+ end
79
+
80
+ # Peek at next job within this tube in given `state`.
81
+ #
82
+ # @param [String] state The job state to peek at (`ready`, `buried`, `delayed`)
83
+ # @return [Tuber::Job] The next job within this tube.
84
+ # @example
85
+ # @tube.peek(:ready) # => <Tuber::Job id=5 body=foo>
86
+ #
87
+ # @api public
88
+ def peek(state)
89
+ safe_use do
90
+ res = transmit("peek-#{state}")
91
+ Job.new(client, res)
92
+ end
93
+ rescue Tuber::NotFoundError
94
+ # Return nil if not found
95
+ nil
96
+ end
97
+
98
+ # Reserves the next job from tube.
99
+ #
100
+ # @param [Integer] timeout Number of seconds before timing out
101
+ # @param [Proc] block Callback to perform on reserved job
102
+ # @yield [job] Job that was reserved.
103
+ # @return [Tuber::Job] Job that was reserved.
104
+ # @example
105
+ # @tube.reserve # => <Tuber::Job id=5 body=foo>
106
+ #
107
+ # @api public
108
+ def reserve(timeout=nil, &block)
109
+ client.tubes.watch!(self.name)
110
+ client.tubes.reserve(timeout, &block)
111
+ end
112
+
113
+ # Kick specified number of jobs from buried to ready state.
114
+ #
115
+ # @param [Integer] bounds The number of jobs to kick.
116
+ # @return [Hash{String => String, Number}] Beanstalkd command response
117
+ # @example
118
+ # @tube.kick(5)
119
+ #
120
+ # @api public
121
+ def kick(bounds=1)
122
+ safe_use { transmit("kick #{bounds}") }
123
+ end
124
+
125
+ # Returns related stats for this tube.
126
+ #
127
+ # @return [Tuber::StatStruct] Struct of tube related values
128
+ # @example
129
+ # @tube.stats.current_jobs_delayed # => 24
130
+ #
131
+ # @api public
132
+ def stats
133
+ res = transmit("stats-tube #{name}")
134
+ StatStruct.from_hash(res[:body])
135
+ end
136
+
137
+ # Pause the execution of this tube for specified `delay`.
138
+ #
139
+ # @param [Integer] delay Number of seconds to delay tube execution
140
+ # @return [Array<Hash{String => String, Number}>] Beanstalkd command response
141
+ # @example
142
+ # @tube.pause(10)
143
+ #
144
+ # @api public
145
+ def pause(delay)
146
+ transmit("pause-tube #{name} #{delay}")
147
+ end
148
+
149
+ # Atomically deletes all jobs from the tube.
150
+ #
151
+ # @return [Integer] Number of jobs flushed
152
+ # @example
153
+ # @tube.flush # => 5
154
+ #
155
+ # @api public
156
+ def flush
157
+ res = transmit("flush-tube #{name}")
158
+ res[:id].to_i
159
+ end
160
+
161
+ # Atomically deletes all buried jobs from the tube. Ready, delayed, and
162
+ # reserved jobs are left untouched. (Tuber only.)
163
+ #
164
+ # @return [Integer] Number of buried jobs flushed
165
+ # @example
166
+ # @tube.flush_buried # => 3
167
+ #
168
+ # @api public
169
+ def flush_buried
170
+ res = transmit("flush-buried #{name}")
171
+ res[:id].to_i
172
+ end
173
+
174
+ # Clears all unreserved jobs in all states from the tube
175
+ #
176
+ # @example
177
+ # @tube.clear
178
+ #
179
+ def clear
180
+ client.tubes.watch!(self.name)
181
+ %w(delayed buried ready).each do |state|
182
+ while job = self.peek(state.to_sym)
183
+ begin
184
+ job.delete
185
+ rescue Tuber::UnexpectedResponse, Tuber::NotFoundError
186
+ # swallow any issues
187
+ end
188
+ end
189
+ end
190
+ client.tubes.ignore(name)
191
+ rescue Tuber::NotIgnoredError
192
+ # swallow any issues
193
+ end
194
+
195
+ # String representation of tube.
196
+ #
197
+ # @return [String] Representation of tube including name.
198
+ # @example
199
+ # @tube.to_s # => "#<Tuber::Tube name=foo>"
200
+ #
201
+ def to_s
202
+ "#<Tuber::Tube name=#{name.inspect}>"
203
+ end
204
+ alias :inspect :to_s
205
+
206
+ protected
207
+
208
+ # Transmits a beanstalk command that requires this tube to be set as used.
209
+ #
210
+ # @param [Proc] block Beanstalk command to transmit.
211
+ # @return [Object] Result of block passed
212
+ # @example
213
+ # safe_use { transmit("kick 1") }
214
+ # # => "Response to kick command"
215
+ #
216
+ def safe_use(&block)
217
+ @mutex.lock
218
+ client.tubes.use(self.name)
219
+ yield
220
+ ensure
221
+ @mutex.unlock
222
+ end
223
+
224
+ # Returns configuration options for tuber
225
+ #
226
+ # @return [Tuber::Configuration] configuration object
227
+ def config
228
+ Tuber.configuration
229
+ end
230
+ end # Tube
231
+ end # Tuber
data/lib/tuber/tube.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tuber/tube/record'
4
+ require 'tuber/tube/collection'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tuber
4
+ # Current version of gem.
5
+ VERSION = "0.6.0"
6
+ end
data/lib/tuber.rb CHANGED
@@ -1,5 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tuber
4
- VERSION = "0.0.1"
3
+ require 'thread' unless defined?(Mutex)
4
+
5
+ %w(version configuration errors connection tube job stats).each do |f|
6
+ require "tuber/#{f}"
5
7
  end
8
+
9
+ class Tuber
10
+
11
+ # @!attribute connection
12
+ # @return <Tuber::Connection> returns the associated connection object
13
+ attr_reader :connection
14
+
15
+ # Initialize new instance of Tuber
16
+ #
17
+ # @param [String] address in the form "host:port"
18
+ # @example
19
+ # Tuber.new('127.0.0.1:11300')
20
+ #
21
+ # ENV['TUBER_URL'] = '127.0.0.1:11300'
22
+ # @b = Tuber.new
23
+ # @b.connection.host # => '127.0.0.1'
24
+ # @b.connection.port # => '11300'
25
+ #
26
+ def initialize(address=nil)
27
+ @connection = Connection.new(address)
28
+ end
29
+
30
+ # Returns Tuber::Tubes object for accessing tube related functions.
31
+ #
32
+ # @return [Tuber::Tubes] tubes object
33
+ # @api public
34
+ def tubes
35
+ @tubes ||= Tuber::Tubes.new(self)
36
+ end
37
+
38
+ # Returns Tuber::Jobs object for accessing job related functions.
39
+ #
40
+ # @return [Tuber::Jobs] jobs object
41
+ # @api public
42
+ def jobs
43
+ @jobs ||= Tuber::Jobs.new(self)
44
+ end
45
+
46
+ # Returns Tuber::Stats object for accessing beanstalk stats.
47
+ #
48
+ # @return [Tuber::Stats] stats object
49
+ # @api public
50
+ def stats
51
+ @stats ||= Stats.new(self)
52
+ end
53
+
54
+ # Puts the server into drain mode, rejecting new puts.
55
+ #
56
+ # @return [Hash] Response from beanstalkd
57
+ # @example
58
+ # @tuber_instance.drain
59
+ #
60
+ def drain
61
+ connection.transmit("drain")
62
+ end
63
+
64
+ # Takes the server out of drain mode, allowing new puts.
65
+ #
66
+ # @return [Hash] Response from beanstalkd
67
+ # @example
68
+ # @tuber_instance.undrain
69
+ #
70
+ def undrain
71
+ connection.transmit("undrain")
72
+ end
73
+
74
+ # Re-establishes the connection and replays its tube state: watched tubes
75
+ # with their weights, the used tube, and the reserve mode.
76
+ #
77
+ # Use this rather than building a new Tuber when a connection drops — a new
78
+ # client starts with no tube state and, by default, gets a single connect
79
+ # attempt, so it loses a race a reconnect would have won.
80
+ #
81
+ # @param [Integer, nil] tries Maximum number of connect attempts, nil for the
82
+ # configured default (see Tuber::Configuration#connect_retries)
83
+ # @param [Numeric, nil] retry_interval Seconds between attempts, nil for the
84
+ # configured default
85
+ # @return [Tuber] self
86
+ # @raise [Tuber::NotConnected] Every connect attempt failed
87
+ # @example
88
+ # @tuber_instance.reconnect!
89
+ #
90
+ def reconnect!(tries: nil, retry_interval: nil)
91
+ connection.reconnect!(tries: tries, retry_interval: retry_interval)
92
+ self
93
+ end
94
+
95
+ # Closes the related connection
96
+ #
97
+ # @example
98
+ # @tuber_instance.close
99
+ #
100
+ def close
101
+ connection.close if connection
102
+ end
103
+
104
+ protected
105
+
106
+ class << self
107
+ # Yields a configuration block
108
+ #
109
+ # @example
110
+ # Tuber.configure do |config|
111
+ # config.job_parser = lamda { |body| Yaml.load(body)}
112
+ # end
113
+ #
114
+ def configure(&block)
115
+ yield(configuration) if block_given?
116
+ configuration
117
+ end
118
+
119
+ # Returns the configuration options set for Backburner
120
+ #
121
+ # @example
122
+ # Tuber.configuration.default_put_ttr => 120
123
+ #
124
+ def configuration
125
+ @_configuration ||= Configuration.new
126
+ end
127
+ end
128
+ end # Tuber