tuber 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile +3 -1
- data/README.md +51 -0
- data/lib/tuber/configuration.rb +18 -0
- data/lib/tuber/connection.rb +157 -23
- data/lib/tuber/tube/collection.rb +6 -2
- data/lib/tuber/version.rb +1 -1
- data/lib/tuber.rb +21 -0
- data/test/connection_test.rb +336 -0
- data/test/tubes_test.rb +15 -2
- metadata +1 -2
- data/TODO +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28e2397d8368c54a6711fa1db82b5c54ea649a4113e4da808b9dbc6db9cec1cb
|
|
4
|
+
data.tar.gz: dee769c5e37ff64726e1563150af3a25e2dc3f65405844f58b76bafbf4f7148f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48a99c28b12b3f83cd1f24f743fabda6f06a97802c3e2633bb805f7309a3574508d45b3ba7c2ce1cfd7842b1f92159115e19f7331362c79d662c687f0b60af75
|
|
7
|
+
data.tar.gz: 60fa0ea0b9d9440ba798bf953f640f9a6c654bf3988bdca58bc51e037bfa347fefc606964c81a236335467cd5441ce4344ef9382a725e2920d69d8a871ba996f
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ The tuber gem is a fork of [beaneater](https://github.com/beanstalkd/beaneater),
|
|
|
4
4
|
beaneater 1.1.4. Entries from 1.1.4 down are beaneater's history, kept for reference.
|
|
5
5
|
(0.0.1 was an empty placeholder published to claim the gem name.)
|
|
6
6
|
|
|
7
|
+
## 0.6.0 (September 16 2026)
|
|
8
|
+
|
|
9
|
+
Reconnection release. The gem retried reconnects but not connects, so a
|
|
10
|
+
consumer that caught `Tuber::NotConnected` and built a new client — the obvious
|
|
11
|
+
thing to write — never reached the retrying path, and a server restart of a few
|
|
12
|
+
seconds was enough to kill it. `reconnect!` makes the existing healing
|
|
13
|
+
reachable, and `connect_retries` gives a cold start a window wider than the
|
|
14
|
+
outage. Existing behaviour is unchanged until `connect_retries` is raised, with
|
|
15
|
+
two exceptions noted below: `NotConnected` messages gained a suffix (the
|
|
16
|
+
historical text remains a strict prefix), and a connect error other than
|
|
17
|
+
`ECONNREFUSED` is now retried and surfaced as `NotConnected` rather than
|
|
18
|
+
escaping as a raw `Errno`.
|
|
19
|
+
|
|
20
|
+
* Add `Tuber#reconnect!` / `Connection#reconnect!`: re-establishes the connection and replays its tube state (watched tubes with their weights, the used tube, the reserve mode). This healing only ever ran when the connection itself noticed a dropped socket, so a consumer that caught `Tuber::NotConnected` had no way to reach it and had to build a new client — which starts with no tube state and has to re-watch and re-weight every tube by hand.
|
|
21
|
+
* Add `connect_retries` and `connect_retry_interval` configuration. The initial connect took exactly one attempt with no retry, so a cold start during a server restart — a web boot, a fresh `Tuber.new`, a daemon rebuilding its client after an error — failed where an established connection would have healed. Defaults to the historical single attempt; raising it also lifts the reconnect budget of connections already established, so one knob covers both.
|
|
22
|
+
* Retry a connect that fails for any reason the server might be unreachable (`ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH`, DNS failure, connect timeout), not only `ECONNREFUSED`. Those previously escaped as a raw `Errno` from the middle of a reconnect, a class no consumer thinks to rescue.
|
|
23
|
+
* `Tuber::NotConnected` now names the underlying error in its message and keeps it as the exception's `cause`. A bare `rescue` collapsed every connect failure into "Connection to beanstalk '...' is closed!", hiding whether the server refused, timed out, or was never resolved — exactly what you need during an outage.
|
|
24
|
+
* `Connection#close` no longer raises if the socket cannot be closed cleanly; the connection is gone either way and `reconnect!` must not fail on the way to replacing it.
|
|
25
|
+
* Fix a reconnect silently dropping per-tube weights and the connection's reserve mode ([#1](https://github.com/tuberq/tuber-gem/issues/1)). Both are per-connection server state that a fresh socket resets, so a consumer that healed across a brief server restart kept working but quietly fell back to FIFO. `Connection` now remembers the weight each tube was watched with and the mode set by `Tubes#reserve_mode`, and replays them (watch, then `ignore default`, then `reserve-mode`) when re-initialising tubes.
|
|
26
|
+
* Fix `_initialize_tubes` sending `ignore default` after a reconnect even when `default` was one of the watched tubes, which dropped it from the watch list.
|
|
27
|
+
* `Connection#add_to_watched` now takes an optional weight as a second argument, and `Connection#tube_weights` / `Connection#reserve_mode` expose the replayed state.
|
|
28
|
+
|
|
7
29
|
## 0.5.1 (August 13 2026)
|
|
8
30
|
|
|
9
31
|
* Fix empty keyword arguments forwarded through `Tube#transmit`, `Tubes#transmit` and `Jobs#transmit` arriving as an extra positional `{}` on Ruby 2.7
|
data/Gemfile
CHANGED
|
@@ -6,7 +6,9 @@ source 'https://rubygems.org'
|
|
|
6
6
|
gemspec
|
|
7
7
|
|
|
8
8
|
group :development do
|
|
9
|
-
|
|
9
|
+
# 3.5.1 fixes CVE-2020-26298 (XSS when processing quotes). The old '~> 1'
|
|
10
|
+
# pin was inherited from beaneater and capped us below the fix.
|
|
11
|
+
gem 'redcarpet', '~> 3.6', '>= 3.5.1'
|
|
10
12
|
gem 'github-markup'
|
|
11
13
|
gem 'yard'
|
|
12
14
|
end
|
data/README.md
CHANGED
|
@@ -72,6 +72,8 @@ Tuber.configure do |config|
|
|
|
72
72
|
# config.resolv_timeout = nil
|
|
73
73
|
# config.read_timeout = nil
|
|
74
74
|
# config.write_timeout = nil
|
|
75
|
+
# config.connect_retries = 0
|
|
76
|
+
# config.connect_retry_interval = 1
|
|
75
77
|
end
|
|
76
78
|
```
|
|
77
79
|
|
|
@@ -90,6 +92,47 @@ The above options are all defaults, so only include a configuration block if you
|
|
|
90
92
|
@tuber.close
|
|
91
93
|
```
|
|
92
94
|
|
|
95
|
+
#### Surviving a server restart
|
|
96
|
+
|
|
97
|
+
An established connection heals itself: when a command notices the socket has
|
|
98
|
+
died, the connection reconnects, replays its tube state, and re-sends the
|
|
99
|
+
command (except for the non-idempotent verbs — `put`, `delete`, `release`,
|
|
100
|
+
`bury`, `touch`, `kick` — where a blind re-send could duplicate a job or act on
|
|
101
|
+
a reservation that died with the old socket, so the original error is raised
|
|
102
|
+
for the caller to decide).
|
|
103
|
+
|
|
104
|
+
That healing gets three connect attempts a second apart. A server restart
|
|
105
|
+
typically takes longer, so give the client a budget that outlasts it:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Tuber.configure do |config|
|
|
109
|
+
config.connect_retries = 15 # extra attempts, 0 = a single attempt
|
|
110
|
+
config.connect_retry_interval = 1 # seconds between them
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`connect_retries` covers both a cold start (`Tuber.new` while the server is
|
|
115
|
+
down) and reconnects on a client you already hold. It defaults to `0` — a
|
|
116
|
+
single attempt — which is the historical behaviour.
|
|
117
|
+
|
|
118
|
+
When a command does surface `Tuber::NotConnected`, heal the client you have
|
|
119
|
+
rather than building a new one:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
begin
|
|
123
|
+
job = @tuber.tubes.reserve
|
|
124
|
+
rescue Tuber::NotConnected
|
|
125
|
+
@tuber.reconnect! # watched tubes, their weights, the used tube and the
|
|
126
|
+
retry # reserve mode all come back with it
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A fresh `Tuber.new` starts with no tube state, so a worker that reconnects that
|
|
131
|
+
way has to re-watch and re-weight every tube by hand — and will silently fall
|
|
132
|
+
back to FIFO if it forgets the reserve mode. `reconnect!` replays all of it.
|
|
133
|
+
It takes the same budget as the config, overridable per call:
|
|
134
|
+
`@tuber.reconnect!(tries: 30, retry_interval: 2)`.
|
|
135
|
+
|
|
93
136
|
### Tubes
|
|
94
137
|
|
|
95
138
|
Tubes are named work queues. Jobs are `put` into the used tube and `reserve`d from watched tubes. Each tube has a _ready_, _delayed_, and _buried_ queue.
|
|
@@ -270,6 +313,10 @@ job = @tuber.tubes.reserve # batch-jobs selected 6x as often as email
|
|
|
270
313
|
|
|
271
314
|
Tubes default to weight 1. Switch back with `reserve_mode(:fifo)`.
|
|
272
315
|
|
|
316
|
+
Weights and reserve mode are per-connection server state. The client remembers
|
|
317
|
+
both and replays them if the connection has to be re-established, so a worker
|
|
318
|
+
that heals across a server blip keeps reserving by weight.
|
|
319
|
+
|
|
273
320
|
### Batch Reserve (Tuber only)
|
|
274
321
|
|
|
275
322
|
Reserve multiple jobs atomically in a single call:
|
|
@@ -342,6 +389,10 @@ Tuber.configure do |config|
|
|
|
342
389
|
config.job_parser = lambda { |body| body }
|
|
343
390
|
config.job_serializer = lambda { |body| body }
|
|
344
391
|
config.tuber_url = 'localhost:11300'
|
|
392
|
+
|
|
393
|
+
# Connect attempts; see "Surviving a server restart" above.
|
|
394
|
+
config.connect_retries = 0
|
|
395
|
+
config.connect_retry_interval = 1
|
|
345
396
|
end
|
|
346
397
|
```
|
|
347
398
|
|
data/lib/tuber/configuration.rb
CHANGED
|
@@ -15,6 +15,21 @@ class Tuber
|
|
|
15
15
|
attr_accessor :read_timeout # socket read timeout in seconds
|
|
16
16
|
attr_accessor :write_timeout # socket write timeout in seconds
|
|
17
17
|
|
|
18
|
+
# How hard a client tries to connect — "how long should this ride out a
|
|
19
|
+
# server restart", in one knob.
|
|
20
|
+
#
|
|
21
|
+
# An established connection has always retried when it notices a dropped
|
|
22
|
+
# socket (Tuber::Connection::MAX_RETRIES attempts), but the *initial*
|
|
23
|
+
# connect got exactly one: a cold start during an outage — a web boot, a
|
|
24
|
+
# fresh Tuber.new, a daemon rebuilding its client after an error — failed
|
|
25
|
+
# where a held connection would have healed.
|
|
26
|
+
#
|
|
27
|
+
# Defaults to that single attempt, so nothing changes until you raise it.
|
|
28
|
+
# Raising it covers the initial connect and lifts the reconnect budget of
|
|
29
|
+
# connections already established along with it.
|
|
30
|
+
attr_accessor :connect_retries # extra connect attempts, 0 = one shot
|
|
31
|
+
attr_accessor :connect_retry_interval # seconds between connect attempts
|
|
32
|
+
|
|
18
33
|
def initialize
|
|
19
34
|
@default_put_delay = 0
|
|
20
35
|
@default_put_pri = 65536
|
|
@@ -26,6 +41,9 @@ class Tuber
|
|
|
26
41
|
@resolv_timeout = nil
|
|
27
42
|
@read_timeout = nil
|
|
28
43
|
@write_timeout = nil
|
|
44
|
+
@connect_retries = 0
|
|
45
|
+
# Matches Tuber::Connection::DEFAULT_RETRY_INTERVAL, which is not loaded yet.
|
|
46
|
+
@connect_retry_interval = 1
|
|
29
47
|
end
|
|
30
48
|
end # Configuration
|
|
31
49
|
end # Tuber
|
data/lib/tuber/connection.rb
CHANGED
|
@@ -33,7 +33,17 @@ class Tuber
|
|
|
33
33
|
# @returns [Array<String>] returns currently watched tube names
|
|
34
34
|
# @!attribute tube_used
|
|
35
35
|
# @returns [String] returns currently used tube name
|
|
36
|
-
|
|
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
|
|
37
47
|
|
|
38
48
|
# Default port value for beanstalk connection
|
|
39
49
|
DEFAULT_PORT = 11300
|
|
@@ -49,6 +59,25 @@ class Tuber
|
|
|
49
59
|
# keeps the transparent retry.
|
|
50
60
|
NON_IDEMPOTENT_COMMANDS = %w[put delete delete-batch release bury touch touch-all kick kick-job].freeze
|
|
51
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
|
+
|
|
52
81
|
# Initializes new connection.
|
|
53
82
|
#
|
|
54
83
|
# @param [String] address beanstalkd instance address.
|
|
@@ -61,15 +90,23 @@ class Tuber
|
|
|
61
90
|
# @b.connection.host # => '127.0.0.1'
|
|
62
91
|
# @b.connection.port # => '11300'
|
|
63
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
|
+
#
|
|
64
96
|
def initialize(address)
|
|
65
97
|
@address = address || _host_from_env || Tuber.configuration.tuber_url
|
|
66
98
|
@mutex = Mutex.new
|
|
67
99
|
@tube_used = 'default'
|
|
68
100
|
@tubes_watched = ['default']
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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)
|
|
73
110
|
end
|
|
74
111
|
|
|
75
112
|
# Send commands to beanstalkd server via connection.
|
|
@@ -170,11 +207,50 @@ class Tuber
|
|
|
170
207
|
#
|
|
171
208
|
def close
|
|
172
209
|
if @connection
|
|
173
|
-
|
|
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
|
|
174
217
|
@connection = nil
|
|
175
218
|
end
|
|
176
219
|
end
|
|
177
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
|
+
|
|
178
254
|
# Returns string representation of job.
|
|
179
255
|
#
|
|
180
256
|
# @example
|
|
@@ -185,15 +261,34 @@ class Tuber
|
|
|
185
261
|
end
|
|
186
262
|
alias :inspect :to_s
|
|
187
263
|
|
|
188
|
-
|
|
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)
|
|
189
269
|
@tubes_watched << tube_name
|
|
190
|
-
@tubes_watched.uniq
|
|
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
|
|
191
277
|
end
|
|
192
278
|
|
|
193
279
|
def remove_from_watched(tube_name)
|
|
280
|
+
@tube_weights.delete(tube_name)
|
|
194
281
|
@tubes_watched.delete(tube_name)
|
|
195
282
|
end
|
|
196
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
|
+
|
|
197
292
|
protected
|
|
198
293
|
|
|
199
294
|
# Establish a connection based on beanstalk address.
|
|
@@ -278,16 +373,24 @@ class Tuber
|
|
|
278
373
|
|
|
279
374
|
private
|
|
280
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.
|
|
281
381
|
def _initialize_tubes
|
|
282
|
-
if @tubes_watched != ['default']
|
|
382
|
+
if @tubes_watched != ['default'] || @tube_weights.any?
|
|
283
383
|
tubes_watched.each do |t|
|
|
284
|
-
|
|
384
|
+
weight = @tube_weights[t]
|
|
385
|
+
transmit(weight ? "watch #{t} #{weight}" : "watch #{t}", init: false)
|
|
285
386
|
end
|
|
286
387
|
|
|
287
|
-
transmit("ignore default", init: false)
|
|
388
|
+
transmit("ignore default", init: false) unless @tubes_watched.include?('default')
|
|
288
389
|
end
|
|
289
390
|
|
|
290
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'
|
|
291
394
|
end
|
|
292
395
|
|
|
293
396
|
# Wrapper method for capturing certain failures and retry the payload block
|
|
@@ -304,7 +407,7 @@ class Tuber
|
|
|
304
407
|
yield
|
|
305
408
|
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE,
|
|
306
409
|
Errno::ECONNREFUSED => ex
|
|
307
|
-
_reconnect(
|
|
410
|
+
_reconnect(tries: _connect_tries, retry_interval: retry_interval)
|
|
308
411
|
_initialize_tubes if init
|
|
309
412
|
raise ex unless retransmit
|
|
310
413
|
retry
|
|
@@ -318,19 +421,37 @@ class Tuber
|
|
|
318
421
|
retry
|
|
319
422
|
end
|
|
320
423
|
|
|
321
|
-
#
|
|
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.
|
|
322
436
|
#
|
|
323
|
-
# @param [Exception] original_exception The exception caused the retry
|
|
324
|
-
# @param [Integer] retry_interval The time to wait before the next reconnect
|
|
325
437
|
# @param [Integer] tries The maximum number of attempts to reconnect
|
|
326
|
-
|
|
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)
|
|
327
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)
|
|
328
451
|
establish_connection
|
|
329
|
-
rescue
|
|
452
|
+
rescue *CONNECT_ERRORS => ex
|
|
330
453
|
tries -= 1
|
|
331
|
-
if tries
|
|
332
|
-
_raise_not_connected!
|
|
333
|
-
end
|
|
454
|
+
_raise_not_connected!(ex) if tries <= 0
|
|
334
455
|
sleep(retry_interval || DEFAULT_RETRY_INTERVAL)
|
|
335
456
|
retry
|
|
336
457
|
end
|
|
@@ -356,10 +477,23 @@ class Tuber
|
|
|
356
477
|
[sec, usec].pack('l_l_')
|
|
357
478
|
end
|
|
358
479
|
|
|
359
|
-
# Raises an error to be triggered when the connection has failed
|
|
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
|
|
360
492
|
# @raise [Tuber::NotConnected] Beanstalkd is no longer connected
|
|
361
|
-
def _raise_not_connected!
|
|
362
|
-
|
|
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
|
|
363
497
|
end
|
|
364
498
|
|
|
365
499
|
end # Connection
|
|
@@ -103,7 +103,11 @@ class Tuber
|
|
|
103
103
|
#
|
|
104
104
|
# @api public
|
|
105
105
|
def reserve_mode(mode)
|
|
106
|
-
transmit("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
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
# Reserves a specific job by its ID.
|
|
@@ -202,7 +206,7 @@ class Tuber
|
|
|
202
206
|
names.each do |t|
|
|
203
207
|
cmd = weight ? "watch #{t} #{weight}" : "watch #{t}"
|
|
204
208
|
transmit cmd
|
|
205
|
-
client.connection.add_to_watched(t)
|
|
209
|
+
client.connection.add_to_watched(t, weight)
|
|
206
210
|
end
|
|
207
211
|
rescue BadFormatError => ex
|
|
208
212
|
raise InvalidTubeName, "Tube in '#{ex.cmd}' is invalid!"
|
data/lib/tuber/version.rb
CHANGED
data/lib/tuber.rb
CHANGED
|
@@ -71,6 +71,27 @@ class Tuber
|
|
|
71
71
|
connection.transmit("undrain")
|
|
72
72
|
end
|
|
73
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
|
+
|
|
74
95
|
# Closes the related connection
|
|
75
96
|
#
|
|
76
97
|
# @example
|
data/test/connection_test.rb
CHANGED
|
@@ -228,6 +228,131 @@ describe Tuber::Connection do
|
|
|
228
228
|
# non-retransmitted command (delete/put) now surfaces as a failure.
|
|
229
229
|
$called = true
|
|
230
230
|
end
|
|
231
|
+
|
|
232
|
+
it "replays tube weights and reserve mode after reconnect" do
|
|
233
|
+
require_tuber!
|
|
234
|
+
client = Tuber.new(tuber_address)
|
|
235
|
+
client.tubes.watch("reconn_heavy", weight: 8)
|
|
236
|
+
client.tubes.watch("reconn_light", weight: 1)
|
|
237
|
+
client.tubes.ignore("default")
|
|
238
|
+
client.tubes.reserve_mode(:weighted)
|
|
239
|
+
|
|
240
|
+
$replayed = []
|
|
241
|
+
$eof_pending = true
|
|
242
|
+
TCPSocket.prepend Module.new {
|
|
243
|
+
def write(data)
|
|
244
|
+
$replayed << data.to_s if $replayed
|
|
245
|
+
super
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def readline
|
|
249
|
+
if $eof_pending
|
|
250
|
+
$eof_pending = false
|
|
251
|
+
raise EOFError
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
super
|
|
255
|
+
end
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
client.connection.transmit("list-tubes-watched")
|
|
259
|
+
sent = $replayed
|
|
260
|
+
|
|
261
|
+
assert_includes sent, "watch reconn_heavy 8\r\n"
|
|
262
|
+
assert_includes sent, "watch reconn_light 1\r\n"
|
|
263
|
+
assert_includes sent, "reserve-mode weighted\r\n"
|
|
264
|
+
# Ordering matters: watch, then drop default, then set the mode.
|
|
265
|
+
assert sent.index("watch reconn_heavy 8\r\n") < sent.index("ignore default\r\n"),
|
|
266
|
+
"expected the watches to be replayed before 'ignore default'"
|
|
267
|
+
assert sent.index("ignore default\r\n") < sent.index("reserve-mode weighted\r\n"),
|
|
268
|
+
"expected 'reserve-mode' to be replayed after the tubes are watched"
|
|
269
|
+
ensure
|
|
270
|
+
# Leave the prepended module inert for the rest of the suite.
|
|
271
|
+
$eof_pending = false
|
|
272
|
+
$replayed = nil
|
|
273
|
+
client.close if client
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "keeps watching default across a reconnect when default is watched" do
|
|
277
|
+
client = Tuber.new(tuber_address)
|
|
278
|
+
client.tubes.watch("reconn_extra")
|
|
279
|
+
|
|
280
|
+
$replayed = []
|
|
281
|
+
$eof_pending = true
|
|
282
|
+
TCPSocket.prepend Module.new {
|
|
283
|
+
def write(data)
|
|
284
|
+
$replayed << data.to_s if $replayed
|
|
285
|
+
super
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def readline
|
|
289
|
+
if $eof_pending
|
|
290
|
+
$eof_pending = false
|
|
291
|
+
raise EOFError
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
super
|
|
295
|
+
end
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
client.connection.transmit("list-tubes-watched")
|
|
299
|
+
sent = $replayed
|
|
300
|
+
|
|
301
|
+
assert_includes sent, "watch reconn_extra\r\n"
|
|
302
|
+
refute_includes sent, "ignore default\r\n"
|
|
303
|
+
assert_equal %w[default reconn_extra].sort,
|
|
304
|
+
client.tubes.watched.map(&:name).sort
|
|
305
|
+
ensure
|
|
306
|
+
$eof_pending = false
|
|
307
|
+
$replayed = nil
|
|
308
|
+
client.close if client
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it "still reserves by weight after a reconnect" do
|
|
312
|
+
require_tuber!
|
|
313
|
+
consumer = Tuber.new(tuber_address)
|
|
314
|
+
consumer.tubes.watch("reconn_heavy", weight: 8)
|
|
315
|
+
consumer.tubes.watch("reconn_light", weight: 1)
|
|
316
|
+
consumer.tubes.ignore("default")
|
|
317
|
+
consumer.tubes.reserve_mode(:weighted)
|
|
318
|
+
|
|
319
|
+
producer = Tuber.new(tuber_address)
|
|
320
|
+
100.times do |i|
|
|
321
|
+
producer.tubes["reconn_heavy"].put("h#{i}")
|
|
322
|
+
producer.tubes["reconn_light"].put("l#{i}")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
$eof_pending = true
|
|
326
|
+
TCPSocket.prepend Module.new {
|
|
327
|
+
def readline
|
|
328
|
+
if $eof_pending
|
|
329
|
+
$eof_pending = false
|
|
330
|
+
raise EOFError
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
super
|
|
334
|
+
end
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
counts = Hash.new(0)
|
|
338
|
+
60.times do
|
|
339
|
+
job = consumer.tubes.reserve(2)
|
|
340
|
+
counts[job.body[0]] += 1
|
|
341
|
+
job.delete
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# 8:1 over interleaved puts is ~53/7; FIFO would be ~30/30.
|
|
345
|
+
assert counts["h"] > 3 * counts["l"],
|
|
346
|
+
"expected weighted reserve after reconnect, got heavy=#{counts["h"]} light=#{counts["l"]}"
|
|
347
|
+
ensure
|
|
348
|
+
$eof_pending = false
|
|
349
|
+
if producer
|
|
350
|
+
producer.connection.transmit("flush-tube reconn_heavy") rescue nil
|
|
351
|
+
producer.connection.transmit("flush-tube reconn_light") rescue nil
|
|
352
|
+
producer.close
|
|
353
|
+
end
|
|
354
|
+
consumer.close if consumer
|
|
355
|
+
end
|
|
231
356
|
end # transmit
|
|
232
357
|
|
|
233
358
|
describe 'for idempotent insert response' do
|
|
@@ -251,6 +376,217 @@ describe Tuber::Connection do
|
|
|
251
376
|
end
|
|
252
377
|
end # idempotent insert response
|
|
253
378
|
|
|
379
|
+
describe 'for #reconnect!' do
|
|
380
|
+
before do
|
|
381
|
+
@host = tuber_address
|
|
382
|
+
@bc = Tuber::Connection.new(@host)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
after do
|
|
386
|
+
@bc.close rescue nil
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it "re-establishes a connection that was closed" do
|
|
390
|
+
@bc.close
|
|
391
|
+
assert_nil @bc.connection
|
|
392
|
+
|
|
393
|
+
@bc.reconnect!
|
|
394
|
+
|
|
395
|
+
assert_kind_of TCPSocket, @bc.connection
|
|
396
|
+
assert_equal 'OK', @bc.transmit('stats')[:status]
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it "replays watched tubes, weights and reserve mode" do
|
|
400
|
+
require_tuber!
|
|
401
|
+
client = Tuber.new(tuber_address)
|
|
402
|
+
client.tubes.watch("reconn_bang_heavy", weight: 8)
|
|
403
|
+
client.tubes.watch("reconn_bang_light", weight: 1)
|
|
404
|
+
client.tubes.ignore("default")
|
|
405
|
+
client.tubes.reserve_mode(:weighted)
|
|
406
|
+
|
|
407
|
+
$replayed = []
|
|
408
|
+
TCPSocket.prepend Module.new {
|
|
409
|
+
def write(data)
|
|
410
|
+
$replayed << data.to_s if $replayed
|
|
411
|
+
super
|
|
412
|
+
end
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
client.connection.reconnect!
|
|
416
|
+
sent = $replayed
|
|
417
|
+
|
|
418
|
+
assert_includes sent, "watch reconn_bang_heavy 8\r\n"
|
|
419
|
+
assert_includes sent, "watch reconn_bang_light 1\r\n"
|
|
420
|
+
assert_includes sent, "ignore default\r\n"
|
|
421
|
+
assert_includes sent, "reserve-mode weighted\r\n"
|
|
422
|
+
assert_equal %w[reconn_bang_heavy reconn_bang_light].sort,
|
|
423
|
+
client.tubes.watched.map(&:name).sort
|
|
424
|
+
ensure
|
|
425
|
+
# Leave the prepended module inert for the rest of the suite.
|
|
426
|
+
$replayed = nil
|
|
427
|
+
client.close if client
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it "retries while the server is still refusing connections" do
|
|
431
|
+
# The outage shape: the socket is gone and the server will not accept
|
|
432
|
+
# for another second or two. A single connect attempt loses that race.
|
|
433
|
+
@bc.close
|
|
434
|
+
real_socket = TCPSocket.new(tuber_address, 11300)
|
|
435
|
+
TCPSocket.stubs(:new).
|
|
436
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
437
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
438
|
+
returns(real_socket)
|
|
439
|
+
|
|
440
|
+
@bc.reconnect!(retry_interval: 0.01)
|
|
441
|
+
|
|
442
|
+
assert_same real_socket, @bc.connection
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
it "retries a connect that fails for a reason other than ECONNREFUSED" do
|
|
446
|
+
# A remote host mid-restart answers EHOSTUNREACH or ETIMEDOUT as readily
|
|
447
|
+
# as ECONNREFUSED; those must not escape as a raw Errno either.
|
|
448
|
+
@bc.close
|
|
449
|
+
real_socket = TCPSocket.new(tuber_address, 11300)
|
|
450
|
+
TCPSocket.stubs(:new).
|
|
451
|
+
raises(Errno::EHOSTUNREACH.new).then.
|
|
452
|
+
returns(real_socket)
|
|
453
|
+
|
|
454
|
+
@bc.reconnect!(retry_interval: 0.01)
|
|
455
|
+
|
|
456
|
+
assert_same real_socket, @bc.connection
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "raises NotConnected naming the underlying error when the server stays down" do
|
|
460
|
+
@bc.close
|
|
461
|
+
TCPSocket.stubs(:new).raises(Errno::ECONNREFUSED.new)
|
|
462
|
+
|
|
463
|
+
err = assert_raises(Tuber::NotConnected) { @bc.reconnect!(retry_interval: 0.01) }
|
|
464
|
+
assert_match(/ECONNREFUSED/, err.message)
|
|
465
|
+
assert_kind_of Errno::ECONNREFUSED, err.cause
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "is reachable from the Tuber instance" do
|
|
469
|
+
client = Tuber.new(tuber_address)
|
|
470
|
+
client.tubes.watch("reconn_delegate")
|
|
471
|
+
|
|
472
|
+
client.reconnect!
|
|
473
|
+
|
|
474
|
+
assert_includes client.tubes.watched.map(&:name), "reconn_delegate"
|
|
475
|
+
ensure
|
|
476
|
+
client.close if client
|
|
477
|
+
end
|
|
478
|
+
end # reconnect!
|
|
479
|
+
|
|
480
|
+
describe 'for connect retries' do
|
|
481
|
+
after do
|
|
482
|
+
Tuber.configure do |config|
|
|
483
|
+
config.connect_retries = 0
|
|
484
|
+
config.connect_retry_interval = Tuber::Connection::DEFAULT_RETRY_INTERVAL
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
it "should make a single connect attempt by default" do
|
|
489
|
+
TCPSocket.expects(:new).times(1).raises(Errno::ECONNREFUSED.new)
|
|
490
|
+
|
|
491
|
+
assert_raises(Tuber::NotConnected) { Tuber::Connection.new('localhost') }
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
it "should retry the initial connect when connect_retries is configured" do
|
|
495
|
+
real_socket = TCPSocket.new(tuber_address, 11300)
|
|
496
|
+
Tuber.configure do |config|
|
|
497
|
+
config.connect_retries = 3
|
|
498
|
+
config.connect_retry_interval = 0.01
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
TCPSocket.stubs(:new).
|
|
502
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
503
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
504
|
+
returns(real_socket)
|
|
505
|
+
|
|
506
|
+
bc = Tuber::Connection.new(tuber_address)
|
|
507
|
+
|
|
508
|
+
assert_same real_socket, bc.connection
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
it "should give up after connect_retries and name the cause" do
|
|
512
|
+
Tuber.configure do |config|
|
|
513
|
+
config.connect_retries = 2
|
|
514
|
+
config.connect_retry_interval = 0.01
|
|
515
|
+
end
|
|
516
|
+
TCPSocket.expects(:new).times(3).raises(Errno::ECONNREFUSED.new)
|
|
517
|
+
|
|
518
|
+
err = assert_raises(Tuber::NotConnected) { Tuber::Connection.new('localhost') }
|
|
519
|
+
assert_match(/ECONNREFUSED/, err.message)
|
|
520
|
+
assert_kind_of Errno::ECONNREFUSED, err.cause
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
it "should lift an established connection's reconnect budget too" do
|
|
524
|
+
# One knob for "how long should a client ride out a server restart":
|
|
525
|
+
# raising connect_retries has to cover the connection you already hold,
|
|
526
|
+
# not only a cold start.
|
|
527
|
+
bc = Tuber::Connection.new(tuber_address)
|
|
528
|
+
real_socket = TCPSocket.new(tuber_address, 11300)
|
|
529
|
+
|
|
530
|
+
Tuber.configure do |config|
|
|
531
|
+
config.connect_retries = 5
|
|
532
|
+
config.connect_retry_interval = 0.01
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# One dropped read, then four refused connects: past the built-in budget
|
|
536
|
+
# of MAX_RETRIES, inside the configured one.
|
|
537
|
+
$eof_pending = true
|
|
538
|
+
TCPSocket.prepend Module.new {
|
|
539
|
+
def readline
|
|
540
|
+
if $eof_pending
|
|
541
|
+
$eof_pending = false
|
|
542
|
+
raise EOFError
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
super
|
|
546
|
+
end
|
|
547
|
+
}
|
|
548
|
+
TCPSocket.stubs(:new).
|
|
549
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
550
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
551
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
552
|
+
raises(Errno::ECONNREFUSED.new).then.
|
|
553
|
+
returns(real_socket)
|
|
554
|
+
|
|
555
|
+
assert_equal 'OK', bc.transmit('stats')[:status]
|
|
556
|
+
assert_same real_socket, bc.connection
|
|
557
|
+
ensure
|
|
558
|
+
$eof_pending = false
|
|
559
|
+
end
|
|
560
|
+
end # connect retries
|
|
561
|
+
|
|
562
|
+
describe 'for NotConnected messages' do
|
|
563
|
+
it "should keep the historical message as a prefix" do
|
|
564
|
+
# Consumers match on this string. The cause is appended, never inserted.
|
|
565
|
+
TCPSocket.stubs(:new).raises(Errno::ECONNREFUSED.new)
|
|
566
|
+
|
|
567
|
+
err = assert_raises(Tuber::NotConnected) { Tuber::Connection.new('localhost:8544') }
|
|
568
|
+
|
|
569
|
+
assert err.message.start_with?("Connection to beanstalk 'localhost:8544' is closed!"),
|
|
570
|
+
"expected the historical message as a prefix, got #{err.message.inspect}"
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it "should not blame an unrelated in-flight exception" do
|
|
574
|
+
# Commands are very often issued from inside someone else's rescue block,
|
|
575
|
+
# where $! is still set. The cause named has to be the one that stopped
|
|
576
|
+
# this connect, not whatever the caller happened to be handling.
|
|
577
|
+
bc = Tuber::Connection.new(tuber_address)
|
|
578
|
+
bc.close
|
|
579
|
+
|
|
580
|
+
err = begin
|
|
581
|
+
raise ArgumentError, "something else entirely"
|
|
582
|
+
rescue ArgumentError
|
|
583
|
+
assert_raises(Tuber::NotConnected) { bc.transmit('stats') }
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
assert_equal "Connection to beanstalk '#{bc.host}:#{bc.port}' is closed!", err.message
|
|
587
|
+
end
|
|
588
|
+
end # NotConnected messages
|
|
589
|
+
|
|
254
590
|
describe 'for #close' do
|
|
255
591
|
before do
|
|
256
592
|
@host = 'localhost'
|
data/test/tubes_test.rb
CHANGED
|
@@ -128,13 +128,13 @@ describe Tuber::Tubes do
|
|
|
128
128
|
|
|
129
129
|
it 'should send watch command with weight' do
|
|
130
130
|
@connection.expects(:transmit).with("watch foo 4").returns({status: "WATCHING", id: "2"})
|
|
131
|
-
@connection.expects(:add_to_watched).with("foo")
|
|
131
|
+
@connection.expects(:add_to_watched).with("foo", 4)
|
|
132
132
|
@tubes.watch('foo', weight: 4)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
it 'should send watch command without weight by default' do
|
|
136
136
|
@connection.expects(:transmit).with("watch foo").returns({status: "WATCHING", id: "2"})
|
|
137
|
-
@connection.expects(:add_to_watched).with("foo")
|
|
137
|
+
@connection.expects(:add_to_watched).with("foo", nil)
|
|
138
138
|
@tubes.watch('foo')
|
|
139
139
|
end
|
|
140
140
|
end # watch with weight
|
|
@@ -149,13 +149,26 @@ describe Tuber::Tubes do
|
|
|
149
149
|
|
|
150
150
|
it 'should send reserve-mode weighted command' do
|
|
151
151
|
@connection.expects(:transmit).with("reserve-mode weighted").returns({status: "USING", id: "weighted"})
|
|
152
|
+
@connection.expects(:reserve_mode=).with(:weighted)
|
|
152
153
|
@tubes.reserve_mode(:weighted)
|
|
153
154
|
end
|
|
154
155
|
|
|
155
156
|
it 'should send reserve-mode fifo command' do
|
|
156
157
|
@connection.expects(:transmit).with("reserve-mode fifo").returns({status: "USING", id: "fifo"})
|
|
158
|
+
@connection.expects(:reserve_mode=).with(:fifo)
|
|
157
159
|
@tubes.reserve_mode(:fifo)
|
|
158
160
|
end
|
|
161
|
+
|
|
162
|
+
it 'should record the mode on the connection so a reconnect can replay it' do
|
|
163
|
+
require_tuber!
|
|
164
|
+
client = Tuber.new(tuber_address)
|
|
165
|
+
begin
|
|
166
|
+
client.tubes.reserve_mode(:weighted)
|
|
167
|
+
assert_equal :weighted, client.connection.reserve_mode
|
|
168
|
+
ensure
|
|
169
|
+
client.close
|
|
170
|
+
end
|
|
171
|
+
end
|
|
159
172
|
end # reserve_mode
|
|
160
173
|
|
|
161
174
|
describe "for #reserve" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tuber
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dan Milne
|
|
@@ -97,7 +97,6 @@ files:
|
|
|
97
97
|
- LICENSE.txt
|
|
98
98
|
- README.md
|
|
99
99
|
- Rakefile
|
|
100
|
-
- TODO
|
|
101
100
|
- examples/demo.rb
|
|
102
101
|
- lib/tuber.rb
|
|
103
102
|
- lib/tuber/configuration.rb
|