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,500 @@
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
+ # @!attribute reserve_mode
37
+ # @returns [String, Symbol, nil] the reserve mode set on this connection,
38
+ # nil when it was never set (the server default is fifo)
39
+ attr_accessor :tube_used, :reserve_mode
40
+ attr_reader :tubes_watched
41
+
42
+ # @!attribute tube_weights
43
+ # @returns [Hash{String => Integer}] weight last watched with, per tube.
44
+ # Tubes watched without a weight are absent: the server resets a tube's
45
+ # weight to 1 on a plain `watch`, so "no weight" is state worth keeping.
46
+ attr_reader :tube_weights
47
+
48
+ # Default port value for beanstalk connection
49
+ DEFAULT_PORT = 11300
50
+
51
+ # Commands that must not be retransmitted after a dropped connection.
52
+ # The socket dying between write and readline leaves the first send's
53
+ # fate unknown: a re-sent put can insert a duplicate job, and a re-sent
54
+ # delete/release/bury/touch acts on a job whose reservation died with
55
+ # the old socket — the server answers NOT_FOUND for work that actually
56
+ # succeeded. For these verbs the connection is healed but the original
57
+ # error is re-raised so the caller decides. Everything else (reserve,
58
+ # watch, stats, peek, ...) converges to the same state on a re-send and
59
+ # keeps the transparent retry.
60
+ NON_IDEMPOTENT_COMMANDS = %w[put delete delete-batch release bury touch touch-all kick kick-job].freeze
61
+
62
+ # Errno names that mean "the server is not reachable right now" during a
63
+ # connect attempt. Every one of them is worth another try: a host coming
64
+ # back from a restart answers EHOSTUNREACH or ETIMEDOUT as readily as
65
+ # ECONNREFUSED, and a raw Errno escaping the connection is a class no
66
+ # consumer thinks to rescue.
67
+ CONNECT_ERROR_NAMES = %w[
68
+ ECONNREFUSED ECONNRESET ECONNABORTED ETIMEDOUT
69
+ EHOSTUNREACH EHOSTDOWN ENETUNREACH ENETDOWN EPIPE
70
+ ].freeze
71
+
72
+ # The resolved exception classes, skipping any Errno the platform lacks.
73
+ # SocketError covers a name that will not resolve (a DNS server restarting
74
+ # alongside the queue), IO::TimeoutError a connect_timeout on Ruby >= 3.2.
75
+ CONNECT_ERRORS = (
76
+ CONNECT_ERROR_NAMES.map { |name| Errno.const_get(name) if Errno.const_defined?(name) }.compact +
77
+ [SocketError] +
78
+ (defined?(IO::TimeoutError) ? [IO::TimeoutError] : [])
79
+ ).freeze
80
+
81
+ # Initializes new connection.
82
+ #
83
+ # @param [String] address beanstalkd instance address.
84
+ # @example
85
+ # Tuber::Connection.new('127.0.0.1')
86
+ # Tuber::Connection.new('127.0.0.1:11300')
87
+ #
88
+ # ENV['TUBER_URL'] = '127.0.0.1:11300'
89
+ # @b = Tuber.new
90
+ # @b.connection.host # => '127.0.0.1'
91
+ # @b.connection.port # => '11300'
92
+ #
93
+ # @raise [Tuber::NotConnected] Could not connect. The underlying error is
94
+ # named in the message and kept as the exception's +cause+.
95
+ #
96
+ def initialize(address)
97
+ @address = address || _host_from_env || Tuber.configuration.tuber_url
98
+ @mutex = Mutex.new
99
+ @tube_used = 'default'
100
+ @tubes_watched = ['default']
101
+ @tube_weights = {}
102
+ @reserve_mode = nil
103
+
104
+ _connect(tries: config.connect_retries.to_i + 1,
105
+ retry_interval: config.connect_retry_interval)
106
+ rescue Tuber::NotConnected
107
+ raise # already carries its cause; do not wrap it twice
108
+ rescue => ex
109
+ _raise_not_connected!(ex)
110
+ end
111
+
112
+ # Send commands to beanstalkd server via connection.
113
+ #
114
+ # @param [Hash{String => String, Number}>] options Retained for compatibility
115
+ # @param [String] command Beanstalkd command
116
+ # @return [Array<Hash{String => String, Number}>] Beanstalkd command response
117
+ # @example
118
+ # @conn = Tuber::Connection.new
119
+ # @conn.transmit('bury 123')
120
+ # @conn.transmit('stats')
121
+ #
122
+ def transmit(command, **options)
123
+ verb = command.to_s[/\A\S+/]
124
+ retransmit = !NON_IDEMPOTENT_COMMANDS.include?(verb)
125
+ _with_retry(retransmit: retransmit, **options.slice(:retry_interval, :init)) do
126
+ @mutex.synchronize do
127
+ _raise_not_connected! unless connection
128
+
129
+ command = command.dup.force_encoding('ASCII-8BIT') if command.respond_to?(:force_encoding)
130
+ connection.write(command.to_s + "\r\n")
131
+ res = connection.readline
132
+ parse_response(command, res)
133
+ end
134
+ end
135
+ end
136
+
137
+ # Reserves a batch of jobs atomically.
138
+ #
139
+ # Without a +timeout+ (or with +timeout+ of 0) the command is non-blocking:
140
+ # it returns whatever is ready immediately, possibly an empty array. With a
141
+ # positive +timeout+ it long-polls, blocking until the first job arrives (up
142
+ # to +timeout+ seconds) and then draining everything ready, up to +count+.
143
+ #
144
+ # @param [Integer] count Maximum number of jobs to reserve
145
+ # @param [Integer] timeout Seconds to long-poll for the first job (nil = non-blocking)
146
+ # @return [Array<Hash>] Array of job hashes with :status, :id, :body keys
147
+ # @raise [Tuber::DeadlineSoonError] A reserved job's TTR is about to expire
148
+ #
149
+ def reserve_batch(count, timeout = nil)
150
+ _with_retry do
151
+ @mutex.synchronize do
152
+ _raise_not_connected! unless connection
153
+
154
+ cmd = timeout ? "reserve-batch #{count} #{timeout}" : "reserve-batch #{count}"
155
+ connection.write(cmd + "\r\n")
156
+
157
+ header = connection.readline.chomp
158
+ status, actual_count_str = header.split(/\s/, 2)
159
+
160
+ raise UnexpectedResponse.from_status(status, cmd) unless status == "RESERVED_BATCH"
161
+
162
+ actual_count = actual_count_str.to_i
163
+ jobs = []
164
+ actual_count.times do
165
+ line = connection.readline.chomp
166
+ _, job_id, bytes_str = line.split(/\s/)
167
+ bytes = bytes_str.to_i
168
+ body = connection.read(bytes)
169
+ crlf = connection.read(2)
170
+ raise ExpectedCrlfError.new("EXPECTED_CRLF", cmd) unless crlf == "\r\n"
171
+
172
+ body = config.job_parser.call(body)
173
+ jobs << { status: "RESERVED", id: job_id, body: body }
174
+ end
175
+ jobs
176
+ end
177
+ end
178
+ end
179
+
180
+ # Deletes a batch of jobs atomically.
181
+ #
182
+ # @param [Array<Integer, String>] ids Job IDs to delete
183
+ # @return [Hash] with :deleted and :not_found counts
184
+ #
185
+ def delete_batch(ids)
186
+ _with_retry(retransmit: false) do
187
+ @mutex.synchronize do
188
+ _raise_not_connected! unless connection
189
+
190
+ cmd = "delete-batch #{ids.join(' ')}"
191
+ connection.write(cmd + "\r\n")
192
+
193
+ res = connection.readline.chomp
194
+ status, deleted, not_found = res.split(/\s/)
195
+
196
+ raise UnexpectedResponse.from_status(status, cmd) unless status == "DELETED_BATCH"
197
+
198
+ { deleted: deleted.to_i, not_found: not_found.to_i }
199
+ end
200
+ end
201
+ end
202
+
203
+ # Close connection with beanstalkd server.
204
+ #
205
+ # @example
206
+ # @conn.close
207
+ #
208
+ def close
209
+ if @connection
210
+ begin
211
+ @connection.close
212
+ rescue StandardError
213
+ # A socket whose peer has already vanished can fail to close
214
+ # cleanly. It is gone either way, and #reconnect! must not raise
215
+ # on the way to replacing it.
216
+ end
217
+ @connection = nil
218
+ end
219
+ end
220
+
221
+ # Re-establishes this connection and replays its tube state onto the new
222
+ # socket: watched tubes with the weights they were watched with, the used
223
+ # tube, and the reserve mode. Safe to call whether the current socket is
224
+ # healthy, dropped, or already closed.
225
+ #
226
+ # This is the same healing the connection performs internally when a
227
+ # command notices the socket has died, exposed for consumers that hold a
228
+ # connection across an outage. Prefer it to building a new Tuber: a fresh
229
+ # Connection makes a single connect attempt by default (see
230
+ # Tuber::Configuration#connect_retries) and starts with no tube state, so
231
+ # the caller has to re-watch and re-weight everything by hand.
232
+ #
233
+ # @param [Integer, nil] tries Maximum number of connect attempts, nil for
234
+ # the configured default (see Tuber::Configuration#connect_retries)
235
+ # @param [Numeric, nil] retry_interval Seconds to wait between attempts,
236
+ # nil for the configured default
237
+ # @return [Tuber::Connection] self
238
+ # @raise [Tuber::NotConnected] Every connect attempt failed
239
+ # @example
240
+ # begin
241
+ # job = @conn.transmit("reserve")
242
+ # rescue Tuber::NotConnected
243
+ # @conn.reconnect! # watches, weights and reserve mode come back with it
244
+ # retry
245
+ # end
246
+ #
247
+ def reconnect!(tries: nil, retry_interval: nil)
248
+ _reconnect(tries: tries || _connect_tries,
249
+ retry_interval: retry_interval || config.connect_retry_interval)
250
+ _initialize_tubes
251
+ self
252
+ end
253
+
254
+ # Returns string representation of job.
255
+ #
256
+ # @example
257
+ # @conn.inspect
258
+ #
259
+ def to_s
260
+ "#<Tuber::Connection host=#{host.inspect} port=#{port.inspect}>"
261
+ end
262
+ alias :inspect :to_s
263
+
264
+ # Records a tube as watched, along with the weight it was watched with.
265
+ #
266
+ # @param [String] tube_name Name of the tube now being watched
267
+ # @param [Integer, nil] weight Weight passed to `watch`, nil for a plain watch
268
+ def add_to_watched(tube_name, weight = nil)
269
+ @tubes_watched << tube_name
270
+ @tubes_watched.uniq!
271
+ if weight
272
+ @tube_weights[tube_name] = weight
273
+ else
274
+ @tube_weights.delete(tube_name)
275
+ end
276
+ @tubes_watched
277
+ end
278
+
279
+ def remove_from_watched(tube_name)
280
+ @tube_weights.delete(tube_name)
281
+ @tubes_watched.delete(tube_name)
282
+ end
283
+
284
+ # Replaces the watched tube list, dropping weights for tubes that are no
285
+ # longer watched.
286
+ def tubes_watched=(tube_names)
287
+ @tubes_watched = tube_names
288
+ @tube_weights.keep_if { |name, _| @tubes_watched.include?(name) }
289
+ @tubes_watched
290
+ end
291
+
292
+ protected
293
+
294
+ # Establish a connection based on beanstalk address.
295
+ #
296
+ # @return [Net::TCPSocket] connection for specified address.
297
+ # @raise [Tuber::NotConnected] Could not connect to specified beanstalkd instance.
298
+ # @example
299
+ # establish_connection('localhost:3005')
300
+ #
301
+ def establish_connection
302
+ @address = address.first if address.is_a?(Array)
303
+ match = address.split(':')
304
+ @host, @port = match[0], Integer(match[1] || DEFAULT_PORT)
305
+
306
+ tcp_opts = { connect_timeout: config.connect_timeout, resolv_timeout: config.resolv_timeout }.compact
307
+
308
+ socket = if RUBY_VERSION >= "3.0" && tcp_opts.any?
309
+ TCPSocket.new(@host, @port, **tcp_opts)
310
+ else
311
+ TCPSocket.new(@host, @port)
312
+ end
313
+
314
+ begin
315
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, _timeval_for(config.read_timeout)) if config.read_timeout
316
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, _timeval_for(config.write_timeout)) if config.write_timeout
317
+ @connection = socket
318
+ rescue
319
+ socket.close rescue nil
320
+ raise
321
+ end
322
+ end
323
+
324
+ # Parses the response and returns the useful beanstalk response.
325
+ # Will read the body if one is indicated by the status.
326
+ #
327
+ # @param [String] cmd Beanstalk command transmitted
328
+ # @param [String] res Telnet command response
329
+ # @return [Array<Hash{String => String, Number}>] Beanstalk response with `status`, `id`, `body`
330
+ # @raise [Tuber::UnexpectedResponse] Response from beanstalk command was an error status
331
+ # @example
332
+ # parse_response("delete 56", "DELETED 56\nFOO")
333
+ # # => { :body => "FOO", :status => "DELETED", :id => 56 }
334
+ #
335
+ def parse_response(cmd, res)
336
+ status = res.chomp
337
+ body_values = status.split(/\s/)
338
+ status = body_values[0]
339
+ if status == "DRAINING" && cmd.strip.start_with?("drain")
340
+ return { status: status }
341
+ end
342
+ raise UnexpectedResponse.from_status(status, cmd) if UnexpectedResponse::ERROR_STATES.include?(status)
343
+ body = nil
344
+ if status == 'FLUSHED'
345
+ return { status: status, id: body_values[1] }
346
+ end
347
+ if ['OK','FOUND', 'RESERVED'].include?(status)
348
+ bytes_size = body_values[-1].to_i
349
+ raw_body = connection.read(bytes_size)
350
+ body = if status == 'OK'
351
+ psych_v4_valid_body = raw_body.gsub(/^(.*?): (.*)$/) { "#{$1}: #{$2.gsub(/[\:\-\~]/, '_')}" }
352
+ YAML.load(psych_v4_valid_body)
353
+ else
354
+ config.job_parser.call(raw_body)
355
+ end
356
+ crlf = connection.read(2) # \r\n
357
+ raise ExpectedCrlfError.new('EXPECTED_CRLF', cmd) if crlf != "\r\n"
358
+ end
359
+ id = body_values[1]
360
+ response = { :status => status }
361
+ response[:id] = id if id
362
+ response[:body] = body if body
363
+ response[:state] = body_values[2] if status == 'INSERTED' && body_values[2]
364
+ response
365
+ end
366
+
367
+ # Returns configuration options for tuber
368
+ #
369
+ # @return [Tuber::Configuration] configuration object
370
+ def config
371
+ Tuber.configuration
372
+ end
373
+
374
+ private
375
+
376
+ # Replays this connection's tube state onto a freshly established socket.
377
+ # Weights and reserve mode are per-connection server state that a new socket
378
+ # resets, so a reconnect that only re-watched the tube names would silently
379
+ # drop a weighted consumer back to FIFO. Order matters: watch, then drop
380
+ # default, then set the mode.
381
+ def _initialize_tubes
382
+ if @tubes_watched != ['default'] || @tube_weights.any?
383
+ tubes_watched.each do |t|
384
+ weight = @tube_weights[t]
385
+ transmit(weight ? "watch #{t} #{weight}" : "watch #{t}", init: false)
386
+ end
387
+
388
+ transmit("ignore default", init: false) unless @tubes_watched.include?('default')
389
+ end
390
+
391
+ transmit("use #{tube_used}", init: false) if @tube_used != 'default'
392
+
393
+ transmit("reserve-mode #{@reserve_mode}", init: false) if @reserve_mode && @reserve_mode.to_s != 'fifo'
394
+ end
395
+
396
+ # Wrapper method for capturing certain failures and retry the payload block
397
+ #
398
+ # @param [Proc] block The command to execute.
399
+ # @param [Integer] retry_interval The time to wait before the next retry
400
+ # @param [Boolean] retransmit Whether the command is safe to re-send after
401
+ # a reconnect. When false the connection is still healed (reconnect +
402
+ # tube re-init) but the original connection error is re-raised.
403
+ # @param [Integer] tries The maximum number of tries in draining mode
404
+ # @return [Object] Result of the block passed
405
+ #
406
+ def _with_retry(retry_interval: DEFAULT_RETRY_INTERVAL, init: true, tries: MAX_RETRIES, retransmit: true, &block)
407
+ yield
408
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE,
409
+ Errno::ECONNREFUSED => ex
410
+ _reconnect(tries: _connect_tries, retry_interval: retry_interval)
411
+ _initialize_tubes if init
412
+ raise ex unless retransmit
413
+ retry
414
+ rescue Tuber::DrainingError
415
+ tries -= 1
416
+ if tries.zero?
417
+ close
418
+ raise
419
+ end
420
+ sleep(retry_interval)
421
+ retry
422
+ end
423
+
424
+ # Connect attempts to allow when healing a connection that was already
425
+ # established. Never fewer than MAX_RETRIES, so the transparent retry
426
+ # behaves exactly as it always has by default; raising connect_retries
427
+ # lifts this with it, making one knob answer "how long should a client
428
+ # ride out a server restart" for cold starts and reconnects alike.
429
+ #
430
+ # @return [Integer] maximum number of connect attempts
431
+ def _connect_tries
432
+ [MAX_RETRIES, config.connect_retries.to_i + 1].max
433
+ end
434
+
435
+ # Drops the current socket and connects again.
436
+ #
437
+ # @param [Integer] tries The maximum number of attempts to reconnect
438
+ # @param [Numeric] retry_interval The time to wait before the next attempt
439
+ # @raise [Tuber::NotConnected] Every attempt failed
440
+ def _reconnect(tries: MAX_RETRIES, retry_interval: DEFAULT_RETRY_INTERVAL)
441
+ close
442
+ _connect(tries: tries, retry_interval: retry_interval)
443
+ end
444
+
445
+ # Connects, retrying while the server is refusing or unreachable.
446
+ #
447
+ # @param [Integer] tries The maximum number of connect attempts
448
+ # @param [Numeric] retry_interval The time to wait between attempts
449
+ # @raise [Tuber::NotConnected] Every attempt failed
450
+ def _connect(tries: MAX_RETRIES, retry_interval: DEFAULT_RETRY_INTERVAL)
451
+ establish_connection
452
+ rescue *CONNECT_ERRORS => ex
453
+ tries -= 1
454
+ _raise_not_connected!(ex) if tries <= 0
455
+ sleep(retry_interval || DEFAULT_RETRY_INTERVAL)
456
+ retry
457
+ end
458
+
459
+ # The host provided by the TUBER_URL environment variable (or BEANSTALKD_URL,
460
+ # honoured for compatibility with beanstalkd tooling), if available.
461
+ #
462
+ # @return [String] A server host address
463
+ # @example
464
+ # ENV['TUBER_URL'] = "localhost:1212"
465
+ # # => 'localhost:1212'
466
+ #
467
+ def _host_from_env
468
+ url = ENV['TUBER_URL'] || ENV['BEANSTALKD_URL']
469
+ url.respond_to?(:length) && url.length > 0 && url.strip
470
+ end
471
+
472
+ # Packs a timeout value (in seconds) into a struct timeval binary string.
473
+ # Supports fractional seconds (e.g., 0.5 => 500000 usec).
474
+ def _timeval_for(timeout)
475
+ sec = timeout.to_i
476
+ usec = ((timeout.to_f - sec) * 1_000_000).to_i
477
+ [sec, usec].pack('l_l_')
478
+ end
479
+
480
+ # Raises an error to be triggered when the connection has failed.
481
+ #
482
+ # The underlying error is named in the message as well as kept as the
483
+ # exception's +cause+. A log line reading only "Connection to beanstalk
484
+ # '...' is closed!" hides whether the server refused, timed out, or never
485
+ # resolved — which is exactly what you want to know during an outage.
486
+ #
487
+ # Only the two callers that are genuinely inside a rescue pass a cause.
488
+ # Defaulting to +$!+ would be wrong: a command issued from inside someone
489
+ # else's rescue block would name whatever they were handling.
490
+ #
491
+ # @param [Exception, nil] cause The error that stopped the connect
492
+ # @raise [Tuber::NotConnected] Beanstalkd is no longer connected
493
+ def _raise_not_connected!(cause = nil)
494
+ message = "Connection to beanstalk '#{@host}:#{@port}' is closed!"
495
+ message += " (#{cause.class}: #{cause.message})" if cause && !cause.is_a?(Tuber::NotConnected)
496
+ raise Tuber::NotConnected, message
497
+ end
498
+
499
+ end # Connection
500
+ 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