tuber 0.5.1 → 0.7.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.
@@ -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'
@@ -288,4 +624,24 @@ describe Tuber::Connection do
288
624
  assert_raises(Tuber::DrainingError) { @bc.transmit("put 0 0 100 4\r\ntest") }
289
625
  end
290
626
  end # drain and undrain
627
+
628
+ describe 'for a put refused with OUT_OF_STORAGE' do
629
+ # Tuber answers OUT_OF_STORAGE once a persistent server's disk budget
630
+ # (--max-storage-bytes) is spent. It must raise like OUT_OF_MEMORY does,
631
+ # not come back as a hash a caller would read as a successful put.
632
+ it "should raise" do
633
+ @bc = Tuber::Connection.new('localhost')
634
+ TCPSocket.any_instance.stubs(:write)
635
+ TCPSocket.any_instance.expects(:readline).returns("OUT_OF_STORAGE\r\n")
636
+ err = begin
637
+ @bc.transmit("put 0 0 100 4\r\ntest")
638
+ nil
639
+ rescue Tuber::UnexpectedResponse => e
640
+ e
641
+ end
642
+ refute_nil err, "OUT_OF_STORAGE came back as a result instead of raising"
643
+ assert_equal 'OUT_OF_STORAGE', err.status
644
+ assert_equal 'OutOfStorageError', err.class.name.split('::').last
645
+ end
646
+ end # OUT_OF_STORAGE
291
647
  end # Tuber::Connection
data/test/errors_test.rb CHANGED
@@ -32,4 +32,11 @@ describe "Tuber::Errors" do
32
32
  assert_equal 'reserve 0', @klazz.cmd
33
33
  assert_equal 'EXPECTED_CRLF', @klazz.status
34
34
  end
35
+
36
+ it 'should raise proper exception for invalid status OUT_OF_STORAGE' do
37
+ @klazz = Tuber::UnexpectedResponse.from_status("OUT_OF_STORAGE", "put 0 0 120 4")
38
+ assert_kind_of(Tuber::UnexpectedResponse, @klazz)
39
+ assert_equal 'OutOfStorageError', @klazz.class.name.split('::').last
40
+ assert_equal 'OUT_OF_STORAGE', @klazz.status
41
+ end
35
42
  end