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.
@@ -0,0 +1,253 @@
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
+ transmit("reserve-mode #{mode}")
107
+ end
108
+
109
+ # Reserves a specific job by its ID.
110
+ #
111
+ # @param [Integer, String] id The job ID to reserve
112
+ # @return [Tuber::Job, nil] The reserved job, or nil if not found
113
+ # @example
114
+ # @client.tubes.reserve_job(123)
115
+ # # => <Tuber::Job id=123 body="foo">
116
+ #
117
+ # @api public
118
+ def reserve_job(id)
119
+ res = transmit("reserve-job #{id}")
120
+ Job.new(client, res)
121
+ rescue Tuber::NotFoundError
122
+ nil
123
+ end
124
+
125
+ # Returns stats for a job group.
126
+ #
127
+ # @param [String] group The group name
128
+ # @return [Tuber::StatStruct] Struct of group stats
129
+ # @example
130
+ # @client.tubes.stats_group('batch-1')
131
+ # # => #<StatStruct name="batch-1" ready=5 reserved=0 delayed=0 buried=0 waiting_jobs=0>
132
+ #
133
+ # @api public
134
+ def stats_group(group)
135
+ res = transmit("stats-group #{group}")
136
+ StatStruct.from_hash(res[:body])
137
+ end
138
+
139
+ # List of all known beanstalk tubes.
140
+ #
141
+ # @return [Array<Tuber::Tube>] List of all beanstalk tubes.
142
+ # @example
143
+ # @client.tubes.all
144
+ # # => [<Tuber::Tube name="tube2">, <Tuber::Tube name="tube3">]
145
+ #
146
+ # @api public
147
+ def all
148
+ transmit('list-tubes')[:body].map do |tube_name|
149
+ Tube.new(client, tube_name)
150
+ end
151
+ end
152
+
153
+ # Calls the given block once for each known beanstalk tube, passing that element as a parameter.
154
+ #
155
+ # @return An Enumerator is returned if no block is given.
156
+ # @example
157
+ # @pool.tubes.each {|t| puts t.name}
158
+ #
159
+ # @api public
160
+ def each(&block)
161
+ all.each(&block)
162
+ end
163
+
164
+ # List of watched beanstalk tubes.
165
+ #
166
+ # @return [Array<Tuber::Tube>] List of watched beanstalk tubes.
167
+ # @example
168
+ # @client.tubes.watched
169
+ # # => [<Tuber::Tube name="tube2">, <Tuber::Tube name="tube3">]
170
+ #
171
+ # @api public
172
+ def watched
173
+ last_watched = transmit('list-tubes-watched')[:body]
174
+ client.connection.tubes_watched = last_watched.dup
175
+ last_watched.map do |tube_name|
176
+ Tube.new(client, tube_name)
177
+ end
178
+ end
179
+
180
+ # Currently used beanstalk tube.
181
+ #
182
+ # @return [Tuber::Tube] Currently used beanstalk tube.
183
+ # @example
184
+ # @client.tubes.used
185
+ # # => <Tuber::Tube name="tube2">
186
+ #
187
+ # @api public
188
+ def used
189
+ last_used = transmit('list-tube-used')[:id]
190
+ Tube.new(client, last_used)
191
+ end
192
+
193
+ # Add specified beanstalkd tubes as watched.
194
+ #
195
+ # @param [*String] names Name of tubes to watch
196
+ # @raise [Tuber::InvalidTubeName] Tube to watch was invalid.
197
+ # @example
198
+ # @client.tubes.watch('foo', 'bar')
199
+ #
200
+ # @api public
201
+ def watch(*names, weight: nil)
202
+ names.each do |t|
203
+ cmd = weight ? "watch #{t} #{weight}" : "watch #{t}"
204
+ transmit cmd
205
+ client.connection.add_to_watched(t)
206
+ end
207
+ rescue BadFormatError => ex
208
+ raise InvalidTubeName, "Tube in '#{ex.cmd}' is invalid!"
209
+ end
210
+
211
+ # Add specified beanstalkd tubes as watched and ignores all other tubes.
212
+ #
213
+ # @param [*String] names Name of tubes to watch
214
+ # @raise [Tuber::InvalidTubeName] Tube to watch was invalid.
215
+ # @example
216
+ # @client.tubes.watch!('foo', 'bar')
217
+ #
218
+ # @api public
219
+ def watch!(*names)
220
+ old_tubes = watched.map(&:name) - names.map(&:to_s)
221
+ watch(*names)
222
+ ignore(*old_tubes)
223
+ end
224
+
225
+ # Ignores specified beanstalkd tubes.
226
+ #
227
+ # @param [*String] names Name of tubes to ignore
228
+ # @example
229
+ # @client.tubes.ignore('foo', 'bar')
230
+ #
231
+ # @api public
232
+ def ignore(*names)
233
+ names.each do |w|
234
+ transmit "ignore #{w}"
235
+ client.connection.remove_from_watched(w)
236
+ end
237
+ end
238
+
239
+ # Set specified tube as used.
240
+ #
241
+ # @param [String] tube Tube to be used.
242
+ # @example
243
+ # @conn.tubes.use("some-tube")
244
+ #
245
+ def use(tube)
246
+ return tube if last_used == tube
247
+ transmit("use #{tube}")
248
+ self.last_used = tube
249
+ rescue BadFormatError
250
+ raise InvalidTubeName, "Tube cannot be named '#{tube}'"
251
+ end
252
+ end # Tubes
253
+ 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.5.1"
6
+ end
data/lib/tuber.rb CHANGED
@@ -1,5 +1,107 @@
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
+ # Closes the related connection
75
+ #
76
+ # @example
77
+ # @tuber_instance.close
78
+ #
79
+ def close
80
+ connection.close if connection
81
+ end
82
+
83
+ protected
84
+
85
+ class << self
86
+ # Yields a configuration block
87
+ #
88
+ # @example
89
+ # Tuber.configure do |config|
90
+ # config.job_parser = lamda { |body| Yaml.load(body)}
91
+ # end
92
+ #
93
+ def configure(&block)
94
+ yield(configuration) if block_given?
95
+ configuration
96
+ end
97
+
98
+ # Returns the configuration options set for Backburner
99
+ #
100
+ # @example
101
+ # Tuber.configuration.default_put_ttr => 120
102
+ #
103
+ def configuration
104
+ @_configuration ||= Configuration.new
105
+ end
106
+ end
107
+ end # Tuber