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 +4 -4
- data/CHANGELOG.md +61 -0
- data/LICENSE +1 -1
- data/README.md +42 -42
- data/lib/hutch/acknowledgements/nack_on_all_failures.rb +1 -1
- data/lib/hutch/adapters/bunny.rb +24 -0
- data/lib/hutch/adapters/march_hare.rb +21 -1
- data/lib/hutch/broker.rb +67 -34
- data/lib/hutch/cli.rb +1 -4
- data/lib/hutch/config.rb +3 -3
- data/lib/hutch/consumer.rb +3 -3
- data/lib/hutch/error_handlers.rb +0 -1
- data/lib/hutch/serializers/json.rb +3 -3
- data/lib/hutch/tracers/datadog.rb +2 -9
- data/lib/hutch/version.rb +1 -1
- data/lib/hutch/worker.rb +39 -4
- data/lib/yard-settings/yard-settings.rb +2 -2
- data/spec/hutch/broker_spec.rb +166 -2
- data/spec/hutch/cli_spec.rb +12 -9
- data/spec/hutch/error_handlers/airbrake_spec.rb +3 -0
- data/spec/hutch/message_spec.rb +1 -1
- data/spec/hutch/worker_spec.rb +62 -8
- data/spec/integration/channel_recovery_spec.rb +71 -23
- data/spec/integration/publish_consume_spec.rb +1 -2
- metadata +6 -36
- data/.github/workflows/test.yml +0 -74
- data/.gitignore +0 -10
- data/.rspec +0 -1
- data/.yardopts +0 -5
- data/AGENTS.md +0 -127
- data/Gemfile +0 -35
- data/Guardfile +0 -14
- data/Rakefile +0 -21
- data/examples/consumer.rb +0 -13
- data/examples/producer.rb +0 -10
- data/hutch.gemspec +0 -27
- data/lib/hutch/error_handlers/sentry_raven.rb +0 -34
- data/spec/hutch/error_handlers/sentry_raven_spec.rb +0 -37
- data/spec/spec_helper.rb +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8978faf08cf85e68cc957d91ff3059b0b3fecb73e018a80ec551c9547be9c99a
|
|
4
|
+
data.tar.gz: 2c1f461463321e705e2464373093135bf307b68dac9f08b284e8c5f6c9e710db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
](http://badge.fury.io/rb/hutch)
|
|
4
|
-
[](https://codeclimate.com/github/gocardless/hutch)
|
|
1
|
+
[](https://badge.fury.io/rb/hutch)
|
|
2
|
+
[](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
|
|
42
|
-
- Hutch requires RabbitMQ 3.
|
|
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](
|
|
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
|
-
](
|
|
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
|
|
114
|
-
and to operate in the [lazy mode](https://www.rabbitmq.com/lazy-queues
|
|
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
|
|
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
|
|
146
|
-
about custom queue arguments, consult the [RabbitMQ documentation on AMQP Protocol Extensions](https://www.rabbitmq.com/extensions
|
|
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](
|
|
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](
|
|
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
|
|
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
|
|
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
|
|
250
|
-
and the consumers are not loaded in development environment you will need to
|
|
251
|
-
trigger
|
|
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
|
-

|
|
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
|
|
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
|
|
358
|
-
|
|
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
|
-
|
|
366
|
-
channel = AMQP::Channel.new(connection)
|
|
367
|
-
exchange = channel.topic(config[:exchange], durable: true)
|
|
363
|
+
require 'bunny'
|
|
368
364
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
end
|
|
372
|
-
```
|
|
365
|
+
conn = Bunny.new(host: config[:host])
|
|
366
|
+
conn.start
|
|
373
367
|
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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](
|
|
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="
|
|
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>
|
|
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>
|
data/lib/hutch/adapters/bunny.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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'] ==
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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] =
|
|
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
|
|
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]
|
|
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
|
|
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
|
-
#
|
|
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](
|
|
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
|
-
#
|
|
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
|
data/lib/hutch/consumer.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
data/lib/hutch/error_handlers.rb
CHANGED
|
@@ -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'
|