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.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require 'rake/testtask'
5
+ require 'yard'
6
+ require 'redcarpet'
7
+
8
+ # rake test
9
+ Rake::TestTask.new do |t|
10
+ t.libs.push "lib"
11
+ t.test_files = FileList[File.expand_path('../test/**/*_test.rb', __FILE__)] -
12
+ FileList[File.expand_path('../test/**/tuber_test.rb', __FILE__)]
13
+ t.verbose = true
14
+ end
15
+
16
+ # rake test:integration
17
+ Rake::TestTask.new("test:integration") do |t|
18
+ t.libs.push "lib"
19
+ t.test_files = FileList[File.expand_path('../test/**/tuber_test.rb', __FILE__)]
20
+ t.verbose = true
21
+ end
22
+
23
+ # rake test:full
24
+ Rake::TestTask.new("test:full") do |t|
25
+ t.libs.push "lib"
26
+ t.test_files = FileList[File.expand_path('../test/**/*_test.rb', __FILE__)]
27
+ t.verbose = true
28
+ end
29
+
30
+ YARD::Rake::YardocTask.new do |t|
31
+ t.files = ['lib/tuber/**/*.rb']
32
+ t.options = []
33
+ end
34
+
35
+ task :default => 'test:full'
data/TODO ADDED
@@ -0,0 +1 @@
1
+ - Remove connection from pool if it's not responding and be able to add more connections and reattempt later
data/examples/demo.rb ADDED
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'term/ansicolor'
4
+ class String; include Term::ANSIColor; end
5
+ def step(msg); "\n[STEP] #{msg}...".yellow; end
6
+ $:.unshift("../lib")
7
+ require 'tuber'
8
+
9
+ # Establish a pool of beanstalks
10
+ puts step("Connecting to Beanstalk")
11
+ bc = Tuber.new('localhost')
12
+ puts bc
13
+
14
+ # Print out key stats
15
+ puts step("Print Stats")
16
+ p bc.stats.keys
17
+ p [bc.stats.total_connections, bc.stats[:total_connections], bc.stats['total_connections']]
18
+
19
+ # find tube
20
+ puts step("Find tube")
21
+ tube = bc.tubes.find('tube2')
22
+ puts tube
23
+
24
+ # Put job onto tube
25
+ puts step("Put job")
26
+ response = tube.put "foo bar", :pri => 1000, :ttr => 10, :delay => 0
27
+ puts response
28
+
29
+ # peek tube
30
+ puts step("Peek tube")
31
+ p tube.peek :ready
32
+
33
+ # watch tube
34
+ bc.tubes.watch!('tube2')
35
+
36
+ # Check tube stats
37
+ puts step("Get tube stats")
38
+ p tube.stats.keys
39
+ p tube.stats.name
40
+ p tube.stats.current_jobs_ready
41
+
42
+ # Reserve job from tube
43
+ puts step("Reserve job")
44
+ p job = bc.tubes.reserve
45
+ jid = job.id
46
+
47
+ # pause tube
48
+ puts step("Pause tube")
49
+ p tube.pause(1)
50
+
51
+ # Register jobs
52
+ puts step("Register jobs for tubes")
53
+ bc.jobs.register('tube_test', :retry_on => [Timeout::Error]) do |job|
54
+ p 'tube_test'
55
+ p job
56
+ raise Tuber::AbortProcessingError
57
+ end
58
+
59
+ bc.jobs.register('tube_test2', :retry_on => [Timeout::Error]) do |job|
60
+ p 'tube_test2'
61
+ p job
62
+ raise Tuber::AbortProcessingError
63
+ end
64
+
65
+ p bc.jobs.processors
66
+
67
+ response = bc.tubes.find('tube_test').put "foo register", :pri => 1000, :ttr => 10, :delay => 0
68
+ response = bc.tubes.find('tube_test2').put "foo baz", :pri => 1000, :ttr => 10, :delay => 0
69
+
70
+ # Process jobs
71
+ puts step("Process jobs")
72
+ 2.times { bc.jobs.process! }
73
+
74
+ # Get job from id (peek job)
75
+ puts step("Get job from id")
76
+ p bc.jobs.find(jid)
77
+ p bc.jobs.peek(jid)
78
+
79
+ # Check job stats
80
+ puts step("Get job stats")
81
+ p job.stats.keys
82
+ p job.stats.tube
83
+ p job.stats.state
84
+
85
+ # bury job
86
+ puts step("Bury job")
87
+ p job.bury
88
+
89
+ # delete job
90
+ puts step("Delete job")
91
+ p job.delete
92
+
93
+ # list tubes
94
+ puts step("List tubes")
95
+ p bc.tubes.watched
96
+ p bc.tubes.used
97
+ p bc.tubes.all
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tuber
4
+ class Configuration
5
+ attr_accessor :default_put_delay # default delay value to put a job
6
+ attr_accessor :default_put_pri # default priority value to put a job
7
+ attr_accessor :default_put_ttr # default ttr value to put a job
8
+ attr_accessor :job_parser # default job_parser to parse job body
9
+ attr_accessor :job_serializer # default serializer for job body
10
+ attr_accessor :tuber_url # default server url
11
+ alias_method :beanstalkd_url, :tuber_url # compatibility with beaneater configs
12
+ alias_method :beanstalkd_url=, :tuber_url=
13
+ attr_accessor :connect_timeout # TCP connect timeout in seconds
14
+ attr_accessor :resolv_timeout # DNS resolve timeout in seconds
15
+ attr_accessor :read_timeout # socket read timeout in seconds
16
+ attr_accessor :write_timeout # socket write timeout in seconds
17
+
18
+ def initialize
19
+ @default_put_delay = 0
20
+ @default_put_pri = 65536
21
+ @default_put_ttr = 120
22
+ @job_parser = lambda { |body| body }
23
+ @job_serializer = lambda { |body| body }
24
+ @tuber_url = nil
25
+ @connect_timeout = nil
26
+ @resolv_timeout = nil
27
+ @read_timeout = nil
28
+ @write_timeout = nil
29
+ end
30
+ end # Configuration
31
+ end # Tuber
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'socket'
5
+
6
+ class Tuber
7
+ # Represents a connection to a beanstalkd instance.
8
+ class Connection
9
+
10
+ # Default number of retries to send a command to a connection
11
+ MAX_RETRIES = 3
12
+
13
+ # Default retry interval
14
+ DEFAULT_RETRY_INTERVAL = 1
15
+
16
+ # @!attribute address
17
+ # @return [String] returns Beanstalkd server address
18
+ # @example
19
+ # @conn.address # => "localhost:11300"
20
+ # @!attribute host
21
+ # @return [String] returns Beanstalkd server host
22
+ # @example
23
+ # @conn.host # => "localhost"
24
+ # @!attribute port
25
+ # @return [Integer] returns Beanstalkd server port
26
+ # @example
27
+ # @conn.port # => "11300"
28
+ # @!attribute connection
29
+ # @return [Net::TCPSocket] returns connection object
30
+ attr_reader :address, :host, :port, :connection
31
+
32
+ # @!attribute tubes_watched
33
+ # @returns [Array<String>] returns currently watched tube names
34
+ # @!attribute tube_used
35
+ # @returns [String] returns currently used tube name
36
+ attr_accessor :tubes_watched, :tube_used
37
+
38
+ # Default port value for beanstalk connection
39
+ DEFAULT_PORT = 11300
40
+
41
+ # Commands that must not be retransmitted after a dropped connection.
42
+ # The socket dying between write and readline leaves the first send's
43
+ # fate unknown: a re-sent put can insert a duplicate job, and a re-sent
44
+ # delete/release/bury/touch acts on a job whose reservation died with
45
+ # the old socket — the server answers NOT_FOUND for work that actually
46
+ # succeeded. For these verbs the connection is healed but the original
47
+ # error is re-raised so the caller decides. Everything else (reserve,
48
+ # watch, stats, peek, ...) converges to the same state on a re-send and
49
+ # keeps the transparent retry.
50
+ NON_IDEMPOTENT_COMMANDS = %w[put delete delete-batch release bury touch touch-all kick kick-job].freeze
51
+
52
+ # Initializes new connection.
53
+ #
54
+ # @param [String] address beanstalkd instance address.
55
+ # @example
56
+ # Tuber::Connection.new('127.0.0.1')
57
+ # Tuber::Connection.new('127.0.0.1:11300')
58
+ #
59
+ # ENV['TUBER_URL'] = '127.0.0.1:11300'
60
+ # @b = Tuber.new
61
+ # @b.connection.host # => '127.0.0.1'
62
+ # @b.connection.port # => '11300'
63
+ #
64
+ def initialize(address)
65
+ @address = address || _host_from_env || Tuber.configuration.tuber_url
66
+ @mutex = Mutex.new
67
+ @tube_used = 'default'
68
+ @tubes_watched = ['default']
69
+
70
+ establish_connection
71
+ rescue
72
+ _raise_not_connected!
73
+ end
74
+
75
+ # Send commands to beanstalkd server via connection.
76
+ #
77
+ # @param [Hash{String => String, Number}>] options Retained for compatibility
78
+ # @param [String] command Beanstalkd command
79
+ # @return [Array<Hash{String => String, Number}>] Beanstalkd command response
80
+ # @example
81
+ # @conn = Tuber::Connection.new
82
+ # @conn.transmit('bury 123')
83
+ # @conn.transmit('stats')
84
+ #
85
+ def transmit(command, **options)
86
+ verb = command.to_s[/\A\S+/]
87
+ retransmit = !NON_IDEMPOTENT_COMMANDS.include?(verb)
88
+ _with_retry(retransmit: retransmit, **options.slice(:retry_interval, :init)) do
89
+ @mutex.synchronize do
90
+ _raise_not_connected! unless connection
91
+
92
+ command = command.dup.force_encoding('ASCII-8BIT') if command.respond_to?(:force_encoding)
93
+ connection.write(command.to_s + "\r\n")
94
+ res = connection.readline
95
+ parse_response(command, res)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Reserves a batch of jobs atomically.
101
+ #
102
+ # Without a +timeout+ (or with +timeout+ of 0) the command is non-blocking:
103
+ # it returns whatever is ready immediately, possibly an empty array. With a
104
+ # positive +timeout+ it long-polls, blocking until the first job arrives (up
105
+ # to +timeout+ seconds) and then draining everything ready, up to +count+.
106
+ #
107
+ # @param [Integer] count Maximum number of jobs to reserve
108
+ # @param [Integer] timeout Seconds to long-poll for the first job (nil = non-blocking)
109
+ # @return [Array<Hash>] Array of job hashes with :status, :id, :body keys
110
+ # @raise [Tuber::DeadlineSoonError] A reserved job's TTR is about to expire
111
+ #
112
+ def reserve_batch(count, timeout = nil)
113
+ _with_retry do
114
+ @mutex.synchronize do
115
+ _raise_not_connected! unless connection
116
+
117
+ cmd = timeout ? "reserve-batch #{count} #{timeout}" : "reserve-batch #{count}"
118
+ connection.write(cmd + "\r\n")
119
+
120
+ header = connection.readline.chomp
121
+ status, actual_count_str = header.split(/\s/, 2)
122
+
123
+ raise UnexpectedResponse.from_status(status, cmd) unless status == "RESERVED_BATCH"
124
+
125
+ actual_count = actual_count_str.to_i
126
+ jobs = []
127
+ actual_count.times do
128
+ line = connection.readline.chomp
129
+ _, job_id, bytes_str = line.split(/\s/)
130
+ bytes = bytes_str.to_i
131
+ body = connection.read(bytes)
132
+ crlf = connection.read(2)
133
+ raise ExpectedCrlfError.new("EXPECTED_CRLF", cmd) unless crlf == "\r\n"
134
+
135
+ body = config.job_parser.call(body)
136
+ jobs << { status: "RESERVED", id: job_id, body: body }
137
+ end
138
+ jobs
139
+ end
140
+ end
141
+ end
142
+
143
+ # Deletes a batch of jobs atomically.
144
+ #
145
+ # @param [Array<Integer, String>] ids Job IDs to delete
146
+ # @return [Hash] with :deleted and :not_found counts
147
+ #
148
+ def delete_batch(ids)
149
+ _with_retry(retransmit: false) do
150
+ @mutex.synchronize do
151
+ _raise_not_connected! unless connection
152
+
153
+ cmd = "delete-batch #{ids.join(' ')}"
154
+ connection.write(cmd + "\r\n")
155
+
156
+ res = connection.readline.chomp
157
+ status, deleted, not_found = res.split(/\s/)
158
+
159
+ raise UnexpectedResponse.from_status(status, cmd) unless status == "DELETED_BATCH"
160
+
161
+ { deleted: deleted.to_i, not_found: not_found.to_i }
162
+ end
163
+ end
164
+ end
165
+
166
+ # Close connection with beanstalkd server.
167
+ #
168
+ # @example
169
+ # @conn.close
170
+ #
171
+ def close
172
+ if @connection
173
+ @connection.close
174
+ @connection = nil
175
+ end
176
+ end
177
+
178
+ # Returns string representation of job.
179
+ #
180
+ # @example
181
+ # @conn.inspect
182
+ #
183
+ def to_s
184
+ "#<Tuber::Connection host=#{host.inspect} port=#{port.inspect}>"
185
+ end
186
+ alias :inspect :to_s
187
+
188
+ def add_to_watched(tube_name)
189
+ @tubes_watched << tube_name
190
+ @tubes_watched.uniq
191
+ end
192
+
193
+ def remove_from_watched(tube_name)
194
+ @tubes_watched.delete(tube_name)
195
+ end
196
+
197
+ protected
198
+
199
+ # Establish a connection based on beanstalk address.
200
+ #
201
+ # @return [Net::TCPSocket] connection for specified address.
202
+ # @raise [Tuber::NotConnected] Could not connect to specified beanstalkd instance.
203
+ # @example
204
+ # establish_connection('localhost:3005')
205
+ #
206
+ def establish_connection
207
+ @address = address.first if address.is_a?(Array)
208
+ match = address.split(':')
209
+ @host, @port = match[0], Integer(match[1] || DEFAULT_PORT)
210
+
211
+ tcp_opts = { connect_timeout: config.connect_timeout, resolv_timeout: config.resolv_timeout }.compact
212
+
213
+ socket = if RUBY_VERSION >= "3.0" && tcp_opts.any?
214
+ TCPSocket.new(@host, @port, **tcp_opts)
215
+ else
216
+ TCPSocket.new(@host, @port)
217
+ end
218
+
219
+ begin
220
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, _timeval_for(config.read_timeout)) if config.read_timeout
221
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, _timeval_for(config.write_timeout)) if config.write_timeout
222
+ @connection = socket
223
+ rescue
224
+ socket.close rescue nil
225
+ raise
226
+ end
227
+ end
228
+
229
+ # Parses the response and returns the useful beanstalk response.
230
+ # Will read the body if one is indicated by the status.
231
+ #
232
+ # @param [String] cmd Beanstalk command transmitted
233
+ # @param [String] res Telnet command response
234
+ # @return [Array<Hash{String => String, Number}>] Beanstalk response with `status`, `id`, `body`
235
+ # @raise [Tuber::UnexpectedResponse] Response from beanstalk command was an error status
236
+ # @example
237
+ # parse_response("delete 56", "DELETED 56\nFOO")
238
+ # # => { :body => "FOO", :status => "DELETED", :id => 56 }
239
+ #
240
+ def parse_response(cmd, res)
241
+ status = res.chomp
242
+ body_values = status.split(/\s/)
243
+ status = body_values[0]
244
+ if status == "DRAINING" && cmd.strip.start_with?("drain")
245
+ return { status: status }
246
+ end
247
+ raise UnexpectedResponse.from_status(status, cmd) if UnexpectedResponse::ERROR_STATES.include?(status)
248
+ body = nil
249
+ if status == 'FLUSHED'
250
+ return { status: status, id: body_values[1] }
251
+ end
252
+ if ['OK','FOUND', 'RESERVED'].include?(status)
253
+ bytes_size = body_values[-1].to_i
254
+ raw_body = connection.read(bytes_size)
255
+ body = if status == 'OK'
256
+ psych_v4_valid_body = raw_body.gsub(/^(.*?): (.*)$/) { "#{$1}: #{$2.gsub(/[\:\-\~]/, '_')}" }
257
+ YAML.load(psych_v4_valid_body)
258
+ else
259
+ config.job_parser.call(raw_body)
260
+ end
261
+ crlf = connection.read(2) # \r\n
262
+ raise ExpectedCrlfError.new('EXPECTED_CRLF', cmd) if crlf != "\r\n"
263
+ end
264
+ id = body_values[1]
265
+ response = { :status => status }
266
+ response[:id] = id if id
267
+ response[:body] = body if body
268
+ response[:state] = body_values[2] if status == 'INSERTED' && body_values[2]
269
+ response
270
+ end
271
+
272
+ # Returns configuration options for tuber
273
+ #
274
+ # @return [Tuber::Configuration] configuration object
275
+ def config
276
+ Tuber.configuration
277
+ end
278
+
279
+ private
280
+
281
+ def _initialize_tubes
282
+ if @tubes_watched != ['default']
283
+ tubes_watched.each do |t|
284
+ transmit("watch #{t}", init: false)
285
+ end
286
+
287
+ transmit("ignore default", init: false)
288
+ end
289
+
290
+ transmit("use #{tube_used}", init: false) if @tube_used != 'default'
291
+ end
292
+
293
+ # Wrapper method for capturing certain failures and retry the payload block
294
+ #
295
+ # @param [Proc] block The command to execute.
296
+ # @param [Integer] retry_interval The time to wait before the next retry
297
+ # @param [Boolean] retransmit Whether the command is safe to re-send after
298
+ # a reconnect. When false the connection is still healed (reconnect +
299
+ # tube re-init) but the original connection error is re-raised.
300
+ # @param [Integer] tries The maximum number of tries in draining mode
301
+ # @return [Object] Result of the block passed
302
+ #
303
+ def _with_retry(retry_interval: DEFAULT_RETRY_INTERVAL, init: true, tries: MAX_RETRIES, retransmit: true, &block)
304
+ yield
305
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE,
306
+ Errno::ECONNREFUSED => ex
307
+ _reconnect(ex, retry_interval)
308
+ _initialize_tubes if init
309
+ raise ex unless retransmit
310
+ retry
311
+ rescue Tuber::DrainingError
312
+ tries -= 1
313
+ if tries.zero?
314
+ close
315
+ raise
316
+ end
317
+ sleep(retry_interval)
318
+ retry
319
+ end
320
+
321
+ # Tries to re-establish connection to the beanstalkd
322
+ #
323
+ # @param [Exception] original_exception The exception caused the retry
324
+ # @param [Integer] retry_interval The time to wait before the next reconnect
325
+ # @param [Integer] tries The maximum number of attempts to reconnect
326
+ def _reconnect(original_exception, retry_interval, tries=MAX_RETRIES)
327
+ close
328
+ establish_connection
329
+ rescue Errno::ECONNREFUSED
330
+ tries -= 1
331
+ if tries.zero?
332
+ _raise_not_connected!
333
+ end
334
+ sleep(retry_interval || DEFAULT_RETRY_INTERVAL)
335
+ retry
336
+ end
337
+
338
+ # The host provided by the TUBER_URL environment variable (or BEANSTALKD_URL,
339
+ # honoured for compatibility with beanstalkd tooling), if available.
340
+ #
341
+ # @return [String] A server host address
342
+ # @example
343
+ # ENV['TUBER_URL'] = "localhost:1212"
344
+ # # => 'localhost:1212'
345
+ #
346
+ def _host_from_env
347
+ url = ENV['TUBER_URL'] || ENV['BEANSTALKD_URL']
348
+ url.respond_to?(:length) && url.length > 0 && url.strip
349
+ end
350
+
351
+ # Packs a timeout value (in seconds) into a struct timeval binary string.
352
+ # Supports fractional seconds (e.g., 0.5 => 500000 usec).
353
+ def _timeval_for(timeout)
354
+ sec = timeout.to_i
355
+ usec = ((timeout.to_f - sec) * 1_000_000).to_i
356
+ [sec, usec].pack('l_l_')
357
+ end
358
+
359
+ # Raises an error to be triggered when the connection has failed
360
+ # @raise [Tuber::NotConnected] Beanstalkd is no longer connected
361
+ def _raise_not_connected!
362
+ raise Tuber::NotConnected, "Connection to beanstalk '#{@host}:#{@port}' is closed!"
363
+ end
364
+
365
+ end # Connection
366
+ end # Tuber
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tuber
4
+ # Raises when a beanstalkd instance is no longer accessible.
5
+ class NotConnected < RuntimeError; end
6
+ # Raises when the tube name specified is invalid.
7
+ class InvalidTubeName < RuntimeError; end
8
+ # Raises when a job has not been reserved properly.
9
+ class JobNotReserved < RuntimeError; end
10
+
11
+ # Abstract class for errors that occur when a command does not complete successfully.
12
+ class UnexpectedResponse < RuntimeError
13
+ # Set of status states that are considered errors
14
+ ERROR_STATES = %w(OUT_OF_MEMORY INTERNAL_ERROR
15
+ BAD_FORMAT UNKNOWN_COMMAND JOB_TOO_BIG DRAINING
16
+ TIMED_OUT DEADLINE_SOON NOT_FOUND NOT_IGNORED EXPECTED_CRLF)
17
+
18
+ # @!attribute status
19
+ # @return [String] returns beanstalkd response status
20
+ # @example @ex.status # => "NOT_FOUND"
21
+ # @!attribute cmd
22
+ # @return [String] returns beanstalkd request command
23
+ # @example @ex.cmd # => "stats-job 23"
24
+ attr_reader :status, :cmd
25
+
26
+ # Initialize unexpected response error
27
+ #
28
+ # @param [Tuber::UnexpectedResponse] status Unexpected response object
29
+ # @param [String] cmd Beanstalkd request command
30
+ #
31
+ # @example
32
+ # Tuber::UnexpectedResponse.new(NotFoundError, 'bury 123')
33
+ #
34
+ def initialize(status, cmd)
35
+ @status, @cmd = status, cmd
36
+ super("Response failed with: #{status}")
37
+ end
38
+
39
+ # Translate beanstalkd error status to ruby Exeception
40
+ #
41
+ # @param [String] status Beanstalkd error status
42
+ # @param [String] cmd Beanstalkd request command
43
+ #
44
+ # @return [Tuber::UnexpectedResponse] Exception for the status provided
45
+ # @example
46
+ # Tuber::UnexpectedResponse.new('NOT_FOUND', 'bury 123')
47
+ #
48
+ def self.from_status(status, cmd)
49
+ error_klazz_name = status.split('_').map { |w| w.capitalize }.join
50
+ error_klazz_name << "Error" unless error_klazz_name =~ /Error$/
51
+ error_klazz = Tuber.const_get(error_klazz_name)
52
+ error_klazz.new(status, cmd)
53
+ end
54
+ end
55
+
56
+ # Raises when the beanstalkd instance runs out of memory
57
+ class OutOfMemoryError < UnexpectedResponse; end
58
+ # Raises when the beanstalkd instance is draining and new jobs cannot be inserted
59
+ class DrainingError < UnexpectedResponse; end
60
+ # Raises when the job or tube cannot be found
61
+ class NotFoundError < UnexpectedResponse; end
62
+ # Raises when the job reserved is going to be released within a second.
63
+ class DeadlineSoonError < UnexpectedResponse; end
64
+ # Raises when a beanstalkd has an internal error.
65
+ class InternalError < UnexpectedResponse; end
66
+ # Raises when a command was not properly formatted.
67
+ class BadFormatError < UnexpectedResponse; end
68
+ # Raises when a command was sent that is unknown.
69
+ class UnknownCommandError < UnexpectedResponse; end
70
+ # Raises when command does not have proper CRLF suffix.
71
+ class ExpectedCrlfError < UnexpectedResponse; end
72
+ # Raises when the body of a job was too large.
73
+ class JobTooBigError < UnexpectedResponse; end
74
+ # Raises when a job was attempted to be reserved but the timeout occurred.
75
+ class TimedOutError < UnexpectedResponse; end
76
+ # Raises when a tube could not be ignored because it is the last watched tube.
77
+ class NotIgnoredError < UnexpectedResponse; end
78
+ end