hutch 1.4.0 → 2.0.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9ca80c1f8e42d188755ed5bc94834a841a3935f962e977f3fe01bc17753bdf7
4
- data.tar.gz: '0149d00db0ebe07503caf6fd955a051cbc71581e2190a44f91c1128a9ebc3744'
3
+ metadata.gz: 8978faf08cf85e68cc957d91ff3059b0b3fecb73e018a80ec551c9547be9c99a
4
+ data.tar.gz: 2c1f461463321e705e2464373093135bf307b68dac9f08b284e8c5f6c9e710db
5
5
  SHA512:
6
- metadata.gz: 5f1432e36ee499275c334b3f39268f2907ab842e766cc0dded754abeaca771692e7a11be8ef3d48c3c2f6e6a6ba1690de5ac5381db5dd0d1cde00cb1f65d7ffa
7
- data.tar.gz: 16d5ef22b69e7468e097a2dacb49b736d27245e586767e013763f577bf69918f0b44f507ddc59c90ba116e9d473eeba3a5acdd8a95cb4b7407d5c899cadc0ebb
6
+ metadata.gz: 8900ac9233d8849f789b2f598e8109d8bcc7aab3bc7870c00d881e47aaa6f3a3722b906a018c8c8f6dd0b69258a15f7f7654027a216429a9bfe2639f53070100
7
+ data.tar.gz: e3c8b5ac42f24483a773167eb79d01e9e29e71859c0a78ccf01d59b018ec1f57a1a5c26c8da47ad9ebb743b04fed9d67989802eb0f495bd894c1b3c51cb830c3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,64 @@
1
+ # Hutch Change Log
2
+
3
+ ## 2.0.0 (TBD)
4
+
5
+ ### Breaking Changes
6
+
7
+ * Ruby `3.2` or JRuby `10` is now required
8
+ * The Datadog tracer now depends on the [`datadog`](https://github.com/DataDog/dd-trace-rb)
9
+ gem, which replaces the EOL `ddtrace`
10
+ ([migration guide](https://github.com/DataDog/dd-trace-rb/blob/master/docs/UpgradeGuide2.md))
11
+ * `Hutch::ErrorHandlers::SentryRaven` has been replaced by
12
+ `Hutch::ErrorHandlers::Sentry`, backed by `sentry-ruby`
13
+ ([migration guide](https://docs.sentry.io/platforms/ruby/migration/))
14
+ * Messages are serialized with the stdlib `json` instead of `multi_json`, which
15
+ means that `MultiJson` configuration such as `MultiJson.use(:oj)` no longer
16
+ affects Hutch
17
+
18
+ ### Consumer Recovery After a RabbitMQ 4.3 Quorum Queue Consumer Timeout
19
+
20
+ RabbitMQ 4.3 quorum queues cancel a timed out consumer with `basic.cancel`
21
+ instead of closing the channel. Hutch 1.4.0 does not react to that, so on 4.3
22
+ a consumer stops consuming its queue until the worker restarts.
23
+
24
+ Workers now re-subscribe consumers cancelled by the server, but only if
25
+ the queue still exists: a `basic.cancel` carries no reason, so a consumer
26
+ timeout is indistinguishable from a queue deletion. The consumer channel is
27
+ replaced in the process, so the deliveries the cancelled consumer still held
28
+ are requeued instead of dropped.
29
+
30
+ Handlers that are still running get a graceful exit to finish before
31
+ the old channel is closed, and their acknowledgements are dropped rather than
32
+ sent on the new channel, where their delivery tags mean nothing.
33
+
34
+ ### Connection URIs Are Now Percent-Decoded
35
+
36
+ The virtual host, username and password of a connection URI are now decoded, as
37
+ [the URI specification](https://www.rabbitmq.com/docs/uri-spec) requires.
38
+ `amqp://host/%2F` connects to the default virtual host instead of one literally
39
+ named `%2F`, and credentials containing a space, `#` or `@` authenticate again.
40
+
41
+ Reported by @deissimon.
42
+
43
+ GitHub issue: [#422](https://github.com/ruby-amqp/hutch/issues/422)
44
+
45
+ ### Fixed Two JRuby Regressions
46
+
47
+ Opening a channel raised, and the `march_hare` adapter did not declare the
48
+ exchange it instantiated.
49
+
50
+ GitHub issue: [#427](https://github.com/ruby-amqp/hutch/issues/427)
51
+
52
+ ### The JRuby Variant Is Revived
53
+
54
+ The `java` platform gem, which depends on March Hare instead of Bunny, was last
55
+ published as `0.25.0` in January 2018 and has seen little more than CI version
56
+ bumps since. This release requires JRuby `10` and `march_hare` `4.7.0`, adds a
57
+ JRuby CI job, and publishes the gem again.
58
+
59
+ Recovering a channel the broker closed remains Bunny-only: `MarchHare::Channel`
60
+ has no `on_error` callback and no `reopen`.
61
+
1
62
  ## 1.4.0 (Apr 7, 2026)
2
63
 
3
64
  ### Ruby 3.0 is Now Required
data/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
1
  Copyright (c) 2013-2016 GoCardless
2
- Copyright (c) 2016-2021 Hutch contributors
2
+ Copyright (c) 2016-2026 Hutch contributors
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person
5
5
  obtaining a copy of this software and associated documentation
data/README.md CHANGED
@@ -1,7 +1,5 @@
1
- ![](http://cl.ly/image/3h0q3F3G142K/hutch.png)
2
-
3
- [![Gem Version](https://badge.fury.io/rb/hutch.svg)](http://badge.fury.io/rb/hutch)
4
- [![Code Climate](https://codeclimate.com/github/gocardless/hutch.svg)](https://codeclimate.com/github/gocardless/hutch)
1
+ [![Gem Version](https://badge.fury.io/rb/hutch.svg)](https://badge.fury.io/rb/hutch)
2
+ [![CI](https://github.com/ruby-amqp/hutch/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-amqp/hutch/actions/workflows/test.yml)
5
3
 
6
4
  Hutch is a Ruby library for enabling asynchronous inter-service communication
7
5
  in a service-oriented architecture, using RabbitMQ.
@@ -23,6 +21,8 @@ gem install hutch
23
21
  - [Message Processing Tracers](#message-processing-tracers)
24
22
  - [Running Hutch](#running-hutch)
25
23
  - [Loading Consumers](#loading-consumers)
24
+ - [Consumer Groups](#consumer-groups)
25
+ - [Loading Consumers Manually (One-by-One)](#loading-consumers-manually-one-by-one)
26
26
  - [Stopping Hutch](#stopping-hutch)
27
27
  - [Producers](#producers)
28
28
  - [Producer Configuration](#producer-configuration)
@@ -38,8 +38,8 @@ gem install hutch
38
38
 
39
39
  ## Requirements
40
40
 
41
- - Hutch requires Ruby 2.4+ or JRuby 9K.
42
- - Hutch requires RabbitMQ 3.3 or later.
41
+ - Hutch requires Ruby 3.2+ or JRuby 10+
42
+ - Hutch requires RabbitMQ 3.13 or later
43
43
 
44
44
  ## Overview
45
45
 
@@ -52,7 +52,7 @@ With Hutch, consumers are stored in separate files and include the `Hutch::Consu
52
52
  They are then loaded by a command line runner which connects to RabbitMQ, sets up queues and bindings,
53
53
  and so on. Publishers connect to RabbitMQ via `Hutch.connect` and publish using `Hutch.publish`.
54
54
 
55
- Hutch uses [Bunny](http://rubybunny.info) or [March Hare](http://rubymarchhare.info)
55
+ Hutch uses [Bunny](https://github.com/ruby-amqp/bunny) or [March Hare](https://github.com/ruby-amqp/march_hare)
56
56
  (on JRuby) under the hood.
57
57
 
58
58
  ### Project Maturity
@@ -79,7 +79,7 @@ message[:id] # => "02ABCXYZ"
79
79
  To subscribe to a topic, pass a routing key to `consume` in the class
80
80
  definition. To bind to multiple routing keys, simply pass extra routing keys
81
81
  in as additional arguments. Refer to the [RabbitMQ docs on topic exchanges
82
- ](http://www.rabbitmq.com/tutorials/tutorial-five-ruby.html) for more information
82
+ ](https://www.rabbitmq.com/tutorials/tutorial-five-ruby) for more information
83
83
  about how to use routing keys. Here's an example consumer:
84
84
 
85
85
  ```ruby
@@ -110,10 +110,10 @@ end
110
110
 
111
111
  It is possible to set some custom options to consumer's queue explicitly.
112
112
  This example sets the consumer's queue as a
113
- [quorum queue](https://www.rabbitmq.com/quorum-queues.html)
114
- and to operate in the [lazy mode](https://www.rabbitmq.com/lazy-queues.html).
113
+ [quorum queue](https://www.rabbitmq.com/docs/quorum-queues)
114
+ and to operate in the [lazy mode](https://www.rabbitmq.com/docs/lazy-queues).
115
115
  The `initial_group_size`
116
- [argument](https://www.rabbitmq.com/quorum-queues.html#replication-factor) is
116
+ [argument](https://www.rabbitmq.com/docs/quorum-queues#replication-factor) is
117
117
  optional.
118
118
 
119
119
  ```ruby
@@ -142,11 +142,11 @@ end
142
142
  ```
143
143
 
144
144
  This sets the `x-max-length` header. For more details, see the [RabbitMQ
145
- documentation on Queue Length Limit](https://www.rabbitmq.com/maxlength.html). To find out more
146
- about custom queue arguments, consult the [RabbitMQ documentation on AMQP Protocol Extensions](https://www.rabbitmq.com/extensions.html).
145
+ documentation on Queue Length Limit](https://www.rabbitmq.com/docs/maxlength). To find out more
146
+ about custom queue arguments, consult the [RabbitMQ documentation on AMQP Protocol Extensions](https://www.rabbitmq.com/docs/extensions).
147
147
 
148
148
  Consumers can write to Hutch's log by calling the logger method. The logger method returns
149
- a [Logger object](http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html).
149
+ a [Logger object](https://docs.ruby-lang.org/en/3.0/Logger.html).
150
150
 
151
151
  ```ruby
152
152
  class FailedPaymentConsumer
@@ -174,7 +174,7 @@ client_logger = Logger.new("/path/to/bunny.log")
174
174
  Hutch::Config.set(:client_logger, client_logger)
175
175
  ```
176
176
 
177
- See this [RabbitMQ tutorial on topic exchanges](http://www.rabbitmq.com/tutorials/tutorial-five-ruby.html)
177
+ See this [RabbitMQ tutorial on topic exchanges](https://www.rabbitmq.com/tutorials/tutorial-five-ruby)
178
178
  to learn more.
179
179
 
180
180
  ### Message Processing Tracers
@@ -206,7 +206,7 @@ usage: hutch [options]
206
206
  --mq-host HOST Set the RabbitMQ host
207
207
  --mq-port PORT Set the RabbitMQ port
208
208
  -t, --[no-]mq-tls Use TLS for the AMQP connection
209
- --mq-tls-cert FILE Certificate for TLS client verification
209
+ --mq-tls-cert FILE Certificate for TLS client verification
210
210
  --mq-tls-key FILE Private key for TLS client verification
211
211
  --mq-exchange EXCHANGE Set the RabbitMQ exchange
212
212
  --mq-vhost VHOST Set the RabbitMQ vhost
@@ -242,13 +242,13 @@ in the config file, allowing for easy customization.
242
242
 
243
243
  Using Hutch with a Rails app is simple. Either start Hutch in the working
244
244
  directory of a Rails app, or pass the path to a Rails app in with the
245
- `--require` option. Consumers defined in Rails apps should be placed with in
245
+ `--require` option. Consumers defined in Rails apps should be placed within
246
246
  the `app/consumers/` directory, to allow them to be auto-loaded when Rails
247
247
  boots.
248
248
 
249
- If you're using the new Zeitwerk autoloader (enabled by default in Rails 6)
250
- and the consumers are not loaded in development environment you will need to
251
- trigger the autoloading in an initializer with
249
+ If you're using the Zeitwerk autoloader (the default since Rails 6)
250
+ and the consumers are not loaded in the development environment, you will need to
251
+ trigger eager loading in an initializer with
252
252
 
253
253
  ```ruby
254
254
  ::Zeitwerk::Loader.eager_load_all
@@ -309,8 +309,6 @@ kill -SIGTERM 456 # or kill -15 456
309
309
  kill -SIGQUIT 789 # or kill -3 789
310
310
  ```
311
311
 
312
- ![](http://g.recordit.co/wyCdzG9Kh3.gif)
313
-
314
312
  ## Producers
315
313
 
316
314
  Hutch includes a `publish` method for sending messages to Hutch consumers. When
@@ -334,7 +332,7 @@ Hutch::Config.set(:mq_exchange, 'name')
334
332
  ### Publisher Confirms
335
333
 
336
334
  For maximum message reliability when producing messages, you can force Hutch to use
337
- [Publisher Confirms](https://www.rabbitmq.com/confirms.html) and wait for a confirmation
335
+ [Publisher Confirms](https://www.rabbitmq.com/docs/confirms) and wait for a confirmation
338
336
  after every message published. This is the safest possible option for publishers
339
337
  but also results in a **significant throughput drop**.
340
338
 
@@ -354,25 +352,27 @@ send messages to Hutch.
354
352
  - Hutch works with topic exchanges, check the producer is also using topic
355
353
  exchanges.
356
354
  - Use message routing keys that match those used in your Hutch consumers.
357
- - Be sure your exchanges are marked as durable. In the Ruby AMQP gem, this is
358
- done by passing `durable: true` to the exchange creation method.
359
- - Publish messages as persistent.
360
- - Using publisher confirms is highly recommended.
355
+ - Be sure your exchanges are marked as durable, by passing `durable: true`
356
+ to the exchange creation method
357
+ - Publish messages as persistent
358
+ - Using publisher confirms is highly recommended
361
359
 
362
360
  Here's an example of a well-behaved publisher, minus publisher confirms:
363
361
 
364
362
  ```ruby
365
- AMQP.connect(host: config[:host]) do |connection|
366
- channel = AMQP::Channel.new(connection)
367
- exchange = channel.topic(config[:exchange], durable: true)
363
+ require 'bunny'
368
364
 
369
- message = JSON.dump({ subject: 'Test', id: 'abc' })
370
- exchange.publish(message, routing_key: 'test', persistent: true)
371
- end
372
- ```
365
+ conn = Bunny.new(host: config[:host])
366
+ conn.start
373
367
 
374
- If using publisher confirms with amqp gem, see [this issue](https://github.com/ruby-amqp/amqp/issues/92)
375
- and [this gist](https://gist.github.com/3042381) for more info.
368
+ ch = conn.create_channel
369
+ exchange = ch.topic(config[:exchange], durable: true)
370
+
371
+ message = JSON.dump({ subject: 'Test', id: 'abc' })
372
+ exchange.publish(message, routing_key: 'test', persistent: true)
373
+
374
+ conn.close
375
+ ```
376
376
 
377
377
  ## Configuration
378
378
 
@@ -385,7 +385,7 @@ Known configuration parameters are:
385
385
  * `mq_host`: RabbitMQ hostname (default: `localhost`)
386
386
  * `mq_port`: RabbitMQ port (default: `5672`)
387
387
  * `mq_vhost`: vhost to use (default: `/`)
388
- * `mq_username`: username to use (default: `guest`, only can connect from localhost as of RabbitMQ 3.3.0)
388
+ * `mq_username`: username to use (default: `guest`, can only connect from localhost)
389
389
  * `mq_password`: password to use (default: `guest`)
390
390
  * `mq_tls`: should TLS be used? (default: `false`)
391
391
  * `mq_tls_cert`: path to client TLS certificate (public key)
@@ -407,7 +407,7 @@ Known configuration parameters are:
407
407
  exception happens, see classes in `Hutch::Acknowledgements`.
408
408
  * `mq_exchange`: exchange to use for publishing (default: `hutch`)
409
409
  * `mq_client_properties`: Bunny's [client properties](https://www.rabbitmq.com/docs/connections#capabilities) (default: `{}`)
410
- * `heartbeat`: [RabbitMQ heartbeat timeout](http://rabbitmq.com/heartbeats.html) (default: `30`)
410
+ * `heartbeat`: [RabbitMQ heartbeat timeout](https://www.rabbitmq.com/docs/heartbeats) (default: `30`)
411
411
  * `connection_timeout`: Bunny's socket open timeout (default: `11`)
412
412
  * `read_timeout`: Bunny's socket read timeout (default: `11`)
413
413
  * `write_timeout`: Bunny's socket write timeout (default: `11`)
@@ -534,7 +534,7 @@ Generate with
534
534
  <td>30</td>
535
535
  <td>Number</td>
536
536
  <td><tt>HUTCH_HEARTBEAT</tt></td>
537
- <td><p><a href="http://rabbitmq.com/heartbeats.html">RabbitMQ heartbeat timeout</a></p></td>
537
+ <td><p><a href="https://www.rabbitmq.com/docs/heartbeats">RabbitMQ heartbeat timeout</a></p></td>
538
538
  </tr>
539
539
  <tr>
540
540
  <td><tt>channel_prefetch</tt></td>
@@ -590,7 +590,7 @@ Generate with
590
590
  <td>11</td>
591
591
  <td>Number</td>
592
592
  <td><tt>HUTCH_GRACEFUL_EXIT_TIMEOUT</tt></td>
593
- <td><p>FIXME: DOCUMENT THIS</p></td>
593
+ <td><p>Timeout (in seconds) for consumer threads to finish before being killed during graceful shutdown</p></td>
594
594
  </tr>
595
595
  <tr>
596
596
  <td><tt>consumer_pool_size</tt></td>
@@ -646,7 +646,7 @@ Generate with
646
646
  <td>false</td>
647
647
  <td>Boolean</td>
648
648
  <td><tt>HUTCH_FORCE_PUBLISHER_CONFIRMS</tt></td>
649
- <td><p>Enables publisher confirms, forces Hutch::Broker#wait_for_confirms for</p></td>
649
+ <td><p>Enables publisher confirms, forces Hutch::Broker#wait_for_confirms for every publish</p></td>
650
650
  </tr>
651
651
  <tr>
652
652
  <td><tt>enable_http_api_use</tt></td>
@@ -681,7 +681,7 @@ Generate with
681
681
  <td>''</td>
682
682
  <td>String</td>
683
683
  <td><tt>HUTCH_GROUP</tt></td>
684
- <td></td>
684
+ <td><p>Consumer group to load (see Consumer Groups)</p></td>
685
685
  </tr>
686
686
  </tbody>
687
687
  </table>
@@ -10,7 +10,7 @@ module Hutch
10
10
  prefix = "message(#{properties.message_id || '-'}): "
11
11
  logger.debug "#{prefix} nacking message"
12
12
 
13
- broker.nack(delivery_info.delivery_tag)
13
+ broker.nack(delivery_info.delivery_tag, channel: delivery_info.channel)
14
14
 
15
15
  true
16
16
  end
@@ -1,14 +1,18 @@
1
1
  require 'bunny'
2
2
  require 'forwardable'
3
3
 
4
+ require 'hutch/logging'
5
+
4
6
  module Hutch
5
7
  module Adapters
6
8
  class BunnyAdapter
7
9
  extend Forwardable
10
+ include Logging
8
11
 
9
12
  DEFAULT_VHOST = Bunny::Session::DEFAULT_VHOST
10
13
 
11
14
  ConnectionRefused = Bunny::TCPConnectionFailed
15
+ ChannelAlreadyClosed = Bunny::ChannelAlreadyClosed
12
16
  PreconditionFailed = Bunny::PreconditionFailed
13
17
 
14
18
  def_delegators :@connection, :start, :disconnect, :close, :create_channel, :open?, :recover_channel_topology
@@ -25,6 +29,26 @@ module Hutch
25
29
  ch.prefetch(prefetch) if prefetch
26
30
  end
27
31
 
32
+ def queue_exists?(name)
33
+ @connection.queue_exists?(name)
34
+ end
35
+
36
+ def install_channel_recovery(ch)
37
+ # A consumer timeout invalidates the delivery tag, so a handler that
38
+ # was still running acknowledges an unknown tag and closes the channel.
39
+ ch.on_error do |channel, close|
40
+ next unless close.delivery_ack_timeout? || close.unknown_delivery_tag?
41
+
42
+ begin
43
+ channel.reopen
44
+ recover_channel_topology(channel)
45
+ logger.warn "recovered consumer channel closed with '#{close.reply_text}'"
46
+ rescue => ex
47
+ logger.error "channel recovery failed: #{ex.class}: #{ex.message}"
48
+ end
49
+ end
50
+ end
51
+
28
52
  def current_timestamp
29
53
  Time.now.to_i
30
54
  end
@@ -9,6 +9,7 @@ module Hutch
9
9
  DEFAULT_VHOST = "/"
10
10
 
11
11
  ConnectionRefused = MarchHare::ConnectionRefused
12
+ ChannelAlreadyClosed = MarchHare::ChannelAlreadyClosed
12
13
  PreconditionFailed = MarchHare::PreconditionFailed
13
14
 
14
15
  def_delegators :@connection, :start, :disconnect, :close, :open?
@@ -25,6 +26,25 @@ module Hutch
25
26
  ch.prefetch = prefetch if prefetch
26
27
  end
27
28
 
29
+ # `MarchHare::Session` has no `queue_exists?` counterpart, and a failed
30
+ # passive declare closes the channel, hence the throwaway one.
31
+ def queue_exists?(name)
32
+ ch = @connection.create_channel
33
+ begin
34
+ ch.queue(name, passive: true)
35
+ true
36
+ rescue MarchHare::NotFound
37
+ false
38
+ ensure
39
+ ch.close rescue nil
40
+ end
41
+ end
42
+
43
+ # MarchHare::Channel has no on_error callback, and neither Channel#reopen
44
+ # nor Session#recover_channel_topology has a `march_hare` counterpart.
45
+ def install_channel_recovery(ch)
46
+ end
47
+
28
48
  def create_channel(n = nil, consumer_pool_size = 1, consumer_pool_abort_on_exception = false)
29
49
  @connection.create_channel(n)
30
50
  end
@@ -34,7 +54,7 @@ module Hutch
34
54
  end
35
55
 
36
56
  def self.new_exchange(ch, exchange_type, exchange_name, exchange_options)
37
- MarchHare::Exchange.new(ch, exchange_name, exchange_options.merge(type: exchange_type))
57
+ MarchHare::Exchange.new(ch, exchange_name, exchange_options.merge(type: exchange_type)).tap(&:declare!)
38
58
  end
39
59
  end
40
60
  end
data/lib/hutch/broker.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'uri'
1
2
  require 'active_support/core_ext/object/blank'
2
3
 
3
4
  require 'carrot-top'
@@ -114,7 +115,7 @@ module Hutch
114
115
  ch.confirm_select
115
116
  end
116
117
 
117
- install_channel_recovery!(ch)
118
+ connection.install_channel_recovery(ch)
118
119
  end
119
120
  end
120
121
 
@@ -122,18 +123,13 @@ module Hutch
122
123
  @channel = open_channel
123
124
  end
124
125
 
125
- def install_channel_recovery!(ch)
126
- ch.on_error do |channel, close|
127
- next unless close.delivery_ack_timeout?
128
-
129
- begin
130
- channel.reopen
131
- connection.recover_channel_topology(channel)
132
- logger.warn 'recovered consumer channel after a delivery acknowledgement timeout'
133
- rescue => ex
134
- logger.error "channel recovery failed: #{ex.class}: #{ex.message}"
135
- end
136
- end
126
+ # Closing the old channel makes RabbitMQ forget the consumers on it
127
+ # and requeue their unacknowledged deliveries.
128
+ def replace_channel!
129
+ close_consumer_channel
130
+ open_channel!
131
+ declare_exchange!
132
+ declare_publisher!
137
133
  end
138
134
 
139
135
  def declare_exchange(ch = channel)
@@ -193,6 +189,10 @@ module Hutch
193
189
  end
194
190
  end
195
191
 
192
+ def queue_exists?(name)
193
+ connection.queue_exists?(name)
194
+ end
195
+
196
196
  # Apply the configured namespace prefix to a queue name.
197
197
  def namespaced_queue_name(name)
198
198
  namespace = @config[:namespace].to_s.downcase.gsub(/[^-_:\.\w]/, "")
@@ -205,7 +205,7 @@ module Hutch
205
205
 
206
206
  filtered = api_client.bindings.
207
207
  reject { |b| b['destination'] == b['routing_key'] }.
208
- select { |b| b['source'] == @config[:mq_exchange] && b['vhost'] == @config[:mq_vhost] }
208
+ select { |b| b['source'] == @config[:mq_exchange] && b['vhost'] == vhost }
209
209
 
210
210
  filtered.each do |binding|
211
211
  results[binding['destination']] << binding['routing_key']
@@ -244,29 +244,26 @@ module Hutch
244
244
  if defined?(JRUBY_VERSION)
245
245
  channel.close
246
246
  else
247
- # Enqueue a failing job that kills the consumer loop
248
- channel_work_pool.shutdown
249
- # Give `timeout` seconds to jobs that are still being processed
250
- channel_work_pool.join(@config[:graceful_exit_timeout])
251
- # If after `timeout` they are still running, they are killed
252
- channel_work_pool.kill
247
+ drain_consumer_work_pool
253
248
  end
254
249
  end
255
250
 
256
- def requeue(delivery_tag)
257
- channel.reject(delivery_tag, true)
251
+ # Delivery tags are scoped to their channel: a tag from a replaced one
252
+ # is dropped, the server has requeued that delivery anyway.
253
+ def requeue(delivery_tag, channel: self.channel)
254
+ channel.reject(delivery_tag, true) if current_channel?(channel)
258
255
  end
259
256
 
260
- def reject(delivery_tag, requeue=false)
261
- channel.reject(delivery_tag, requeue)
257
+ def reject(delivery_tag, requeue = false, channel: self.channel)
258
+ channel.reject(delivery_tag, requeue) if current_channel?(channel)
262
259
  end
263
260
 
264
- def ack(delivery_tag)
265
- channel.ack(delivery_tag, false)
261
+ def ack(delivery_tag, channel: self.channel)
262
+ channel.ack(delivery_tag, false) if current_channel?(channel)
266
263
  end
267
264
 
268
- def nack(delivery_tag)
269
- channel.nack(delivery_tag, false, false)
265
+ def nack(delivery_tag, channel: self.channel)
266
+ channel.nack(delivery_tag, false, false) if current_channel?(channel)
270
267
  end
271
268
 
272
269
  def publish(*args)
@@ -288,6 +285,27 @@ module Hutch
288
285
 
289
286
  private
290
287
 
288
+ def current_channel?(ch)
289
+ ch.equal?(channel)
290
+ end
291
+
292
+ def close_consumer_channel
293
+ return unless @channel && @channel.open?
294
+
295
+ drain_consumer_work_pool unless defined?(JRUBY_VERSION)
296
+ @channel.close
297
+ rescue Hutch::Adapter::ChannelAlreadyClosed
298
+ # The server closed the channel first: same outcome.
299
+ end
300
+
301
+ # Bunny kills the consumer work pool when the channel closes. Draining
302
+ # first gives the running handlers `graceful_exit_timeout` to finish.
303
+ def drain_consumer_work_pool
304
+ channel_work_pool.shutdown
305
+ channel_work_pool.join(@config[:graceful_exit_timeout])
306
+ channel_work_pool.kill
307
+ end
308
+
291
309
  Config = Struct.new(:host, :port, :username, :password, :ssl, :protocol, :sanitized_uri)
292
310
  private_constant :Config
293
311
 
@@ -299,7 +317,7 @@ module Hutch
299
317
  config.password = @config[:mq_password]
300
318
  config.ssl = @config[:mq_api_ssl]
301
319
  config.protocol = config.ssl ? "https://" : "http://"
302
- config.sanitized_uri = "#{config.protocol}#{config.username}@#{config.host}:#{config.port}/"
320
+ config.sanitized_uri = "#{config.protocol}#{URI.encode_uri_component(config.username.to_s)}@#{config.host}:#{config.port}/"
303
321
  end
304
322
  end
305
323
 
@@ -309,7 +327,7 @@ module Hutch
309
327
  {}.tap do |params|
310
328
  params[:host] = @config[:mq_host]
311
329
  params[:port] = @config[:mq_port]
312
- params[:vhost] = @config[:mq_vhost].presence || Hutch::Adapter::DEFAULT_VHOST
330
+ params[:vhost] = vhost
313
331
  params[:auth_mechanism] = @config[:mq_auth_mechanism]
314
332
  params[:username] = @config[:mq_username]
315
333
  params[:password] = @config[:mq_password]
@@ -343,9 +361,23 @@ module Hutch
343
361
  @config[:mq_tls] = u.scheme == 'amqps'
344
362
  @config[:mq_host] = u.host
345
363
  @config[:mq_port] = u.port || default_mq_port
346
- @config[:mq_vhost] = u.path.sub(/^\//, "")
347
- @config[:mq_username] = u.user
348
- @config[:mq_password] = u.password
364
+ @config[:mq_vhost] = parse_vhost(u)
365
+ @config[:mq_username] = u.user && URI.decode_uri_component(u.user)
366
+ @config[:mq_password] = u.password && URI.decode_uri_component(u.password)
367
+ end
368
+
369
+ # Resolved here rather than in #connection_params, so that
370
+ # Config[:mq_vhost] holds the name the HTTP API reports too.
371
+ def parse_vhost(uri)
372
+ URI.decode_uri_component(uri.path.sub(%r{\A/}, "")).presence ||
373
+ Hutch::Adapter::DEFAULT_VHOST
374
+ end
375
+
376
+ # The vhost Hutch connects to. Config[:mq_vhost] can still be blank when it
377
+ # was set directly rather than through a URI, while the broker and its HTTP
378
+ # API call the default vhost `/`.
379
+ def vhost
380
+ @config[:mq_vhost].presence || Hutch::Adapter::DEFAULT_VHOST
349
381
  end
350
382
 
351
383
  def default_mq_port
@@ -356,7 +388,8 @@ module Hutch
356
388
  p = connection_params
357
389
  scheme = p[:tls] ? "amqps" : "amqp"
358
390
 
359
- "#{scheme}://#{p[:username]}@#{p[:host]}:#{p[:port]}/#{p[:vhost].sub(/^\//, '')}"
391
+ "#{scheme}://#{URI.encode_uri_component(p[:username].to_s)}" \
392
+ "@#{p[:host]}:#{p[:port]}/#{URI.encode_uri_component(p[:vhost].sub(%r{\A/}, ''))}"
360
393
  end
361
394
 
362
395
  def with_authentication_error_handler
data/lib/hutch/cli.rb CHANGED
@@ -36,12 +36,9 @@ module Hutch
36
36
  set_up_code_paths!
37
37
 
38
38
  # Because of the order things are required when we run the Hutch binary
39
- # in hutch/bin, the Sentry Raven gem gets required **after** the error
39
+ # in hutch/bin, the sentry-ruby gem gets required **after** the error
40
40
  # handlers are set up. Due to this, we never got any Sentry notifications
41
41
  # when an error occurred in any of the consumers.
42
- if defined?(Raven)
43
- Hutch::Config[:error_handlers] << Hutch::ErrorHandlers::SentryRaven.new
44
- end
45
42
  if defined?(Sentry)
46
43
  Hutch::Config[:error_handlers] << Hutch::ErrorHandlers::Sentry.new
47
44
  end
data/lib/hutch/config.rb CHANGED
@@ -57,7 +57,7 @@ module Hutch
57
57
 
58
58
  # RabbitMQ username to use.
59
59
  #
60
- # As of RabbitMQ 3.3.0, <tt>guest</tt> can only can connect from localhost.
60
+ # The <tt>guest</tt> user can only connect from localhost.
61
61
  string_setting :mq_username, 'guest'
62
62
 
63
63
  # RabbitMQ password
@@ -78,7 +78,7 @@ module Hutch
78
78
  # RabbitMQ HTTP API port
79
79
  number_setting :mq_api_port, 15672
80
80
 
81
- # [RabbitMQ heartbeat timeout](http://rabbitmq.com/heartbeats.html)
81
+ # [RabbitMQ heartbeat timeout](https://www.rabbitmq.com/docs/heartbeats)
82
82
  number_setting :heartbeat, 30
83
83
 
84
84
  # The <tt>basic.qos</tt> prefetch value to use.
@@ -104,7 +104,7 @@ module Hutch
104
104
  # Bunny's reconnect interval
105
105
  number_setting :network_recovery_interval, 1
106
106
 
107
- # FIXME: DOCUMENT THIS
107
+ # Timeout (in seconds) for consumer threads to finish before being killed during graceful shutdown
108
108
  number_setting :graceful_exit_timeout, 11
109
109
 
110
110
  # Bunny consumer work pool size
@@ -14,12 +14,12 @@ module Hutch
14
14
 
15
15
  def reject!
16
16
  @message_rejected = true
17
- broker.reject(delivery_info.delivery_tag)
17
+ broker.reject(delivery_info.delivery_tag, channel: delivery_info.channel)
18
18
  end
19
19
 
20
20
  def requeue!
21
21
  @message_rejected = true
22
- broker.requeue(delivery_info.delivery_tag)
22
+ broker.requeue(delivery_info.delivery_tag, channel: delivery_info.channel)
23
23
  end
24
24
 
25
25
  def message_rejected?
@@ -80,7 +80,7 @@ module Hutch
80
80
  @arguments = arguments
81
81
  end
82
82
 
83
- # Congfiures queue options that will be passed when declaring the queue.
83
+ # Configures queue options that will be passed when declaring the queue.
84
84
  def queue_options(options = {})
85
85
  @queue_options = options
86
86
  end
@@ -2,7 +2,6 @@ module Hutch
2
2
  module ErrorHandlers
3
3
  autoload :Logger, 'hutch/error_handlers/logger'
4
4
  autoload :Sentry, 'hutch/error_handlers/sentry'
5
- autoload :SentryRaven, 'hutch/error_handlers/sentry_raven'
6
5
  autoload :Honeybadger, 'hutch/error_handlers/honeybadger'
7
6
  autoload :Airbrake, 'hutch/error_handlers/airbrake'
8
7
  autoload :Rollbar, 'hutch/error_handlers/rollbar'