hutch 1.3.1 → 1.4.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/.github/workflows/test.yml +47 -29
- data/AGENTS.md +127 -0
- data/CHANGELOG.md +50 -7
- data/Gemfile +2 -2
- data/bin/ci/before_build_docker.sh +2 -7
- data/hutch.gemspec +3 -3
- data/lib/hutch/adapters/bunny.rb +1 -1
- data/lib/hutch/broker.rb +28 -6
- data/lib/hutch/consumer.rb +9 -0
- data/lib/hutch/error_handlers/sentry_raven.rb +3 -0
- data/lib/hutch/tracers/datadog.rb +9 -2
- data/lib/hutch/version.rb +1 -1
- data/lib/hutch/worker.rb +4 -2
- data/spec/hutch/broker_spec.rb +23 -9
- data/spec/hutch/consumer_spec.rb +11 -0
- data/spec/hutch/error_handlers/airbrake_spec.rb +1 -1
- data/spec/hutch/error_handlers/bugsnag_spec.rb +1 -1
- data/spec/hutch/error_handlers/honeybadger_spec.rb +1 -1
- data/spec/hutch/error_handlers/logger_spec.rb +1 -1
- data/spec/hutch/error_handlers/rollbar_spec.rb +1 -1
- data/spec/hutch/error_handlers/sentry_raven_spec.rb +1 -1
- data/spec/hutch/error_handlers/sentry_spec.rb +1 -1
- data/spec/hutch/tracers/datadog_spec.rb +1 -1
- data/spec/hutch/worker_spec.rb +20 -4
- data/spec/integration/channel_recovery_spec.rb +139 -0
- data/spec/integration/publish_consume_spec.rb +48 -0
- metadata +13 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9ca80c1f8e42d188755ed5bc94834a841a3935f962e977f3fe01bc17753bdf7
|
|
4
|
+
data.tar.gz: '0149d00db0ebe07503caf6fd955a051cbc71581e2190a44f91c1128a9ebc3744'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f1432e36ee499275c334b3f39268f2907ab842e766cc0dded754abeaca771692e7a11be8ef3d48c3c2f6e6a6ba1690de5ac5381db5dd0d1cde00cb1f65d7ffa
|
|
7
|
+
data.tar.gz: 16d5ef22b69e7468e097a2dacb49b736d27245e586767e013763f577bf69918f0b44f507ddc59c90ba116e9d473eeba3a5acdd8a95cb4b7407d5c899cadc0ebb
|
data/.github/workflows/test.yml
CHANGED
|
@@ -1,50 +1,68 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: CI
|
|
2
2
|
|
|
3
3
|
concurrency:
|
|
4
4
|
group: ${{ github.ref }}
|
|
5
5
|
cancel-in-progress: true
|
|
6
6
|
|
|
7
|
-
on:
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- "main"
|
|
11
|
+
pull_request:
|
|
12
|
+
branches:
|
|
13
|
+
- "main"
|
|
14
|
+
workflow_dispatch:
|
|
8
15
|
|
|
9
16
|
jobs:
|
|
10
17
|
test:
|
|
11
|
-
|
|
18
|
+
name: RabbitMQ ${{ matrix.rabbitmq }}, Ruby ${{ matrix.ruby }}
|
|
12
19
|
runs-on: ubuntu-latest
|
|
13
20
|
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
ruby:
|
|
25
|
+
- "4.0"
|
|
26
|
+
- "3.4"
|
|
27
|
+
- "3.3"
|
|
28
|
+
- "3.2"
|
|
29
|
+
- "3.1"
|
|
30
|
+
- "3.0"
|
|
31
|
+
rabbitmq:
|
|
32
|
+
- "4.2"
|
|
33
|
+
- "4.1"
|
|
34
|
+
- "4.0"
|
|
35
|
+
- "3.13"
|
|
36
|
+
|
|
37
|
+
env:
|
|
38
|
+
CI: true
|
|
39
|
+
|
|
14
40
|
services:
|
|
15
41
|
rabbitmq:
|
|
16
|
-
image: rabbitmq
|
|
42
|
+
image: rabbitmq:${{ matrix.rabbitmq }}-management
|
|
17
43
|
ports:
|
|
18
44
|
- 5672:5672
|
|
19
45
|
- 15672:15672
|
|
20
46
|
options: --name rabbitmq
|
|
21
47
|
|
|
22
|
-
strategy:
|
|
23
|
-
fail-fast: false
|
|
24
|
-
matrix:
|
|
25
|
-
ruby-version:
|
|
26
|
-
- '2.7'
|
|
27
|
-
- '3.0'
|
|
28
|
-
- '3.1'
|
|
29
|
-
- '3.2'
|
|
30
|
-
- '3.3'
|
|
31
|
-
|
|
32
48
|
steps:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
./bin/ci/before_build_docker.sh
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
|
52
|
+
uses: ruby/setup-ruby@v1
|
|
53
|
+
with:
|
|
54
|
+
ruby-version: ${{ matrix.ruby }}
|
|
55
|
+
bundler-cache: true
|
|
56
|
+
|
|
57
|
+
- name: Wait for RabbitMQ to start
|
|
58
|
+
run: sleep 10
|
|
59
|
+
|
|
60
|
+
- name: Set up RabbitMQ
|
|
61
|
+
run: ./bin/ci/before_build_docker.sh
|
|
62
|
+
|
|
63
|
+
- name: Run tests
|
|
64
|
+
run: bundle exec rspec spec
|
|
65
|
+
|
|
48
66
|
test_all:
|
|
49
67
|
if: ${{ always() }}
|
|
50
68
|
runs-on: ubuntu-latest
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Instructions for AI Agents
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Hutch is a Ruby library for enabling asynchronous inter-service communication
|
|
6
|
+
using RabbitMQ. It provides a conventions-based framework for consumers and
|
|
7
|
+
producers using topic exchanges.
|
|
8
|
+
|
|
9
|
+
Its key dependencies are [Bunny](https://github.com/ruby-amqp/bunny) (MRI Ruby)
|
|
10
|
+
or [March Hare](https://github.com/ruby-amqp/march_hare) (JRuby), two RabbitMQ clients that use AMQP 0-9-1.
|
|
11
|
+
|
|
12
|
+
On top, Hutch uses [carrot-top](https://github.com/change/carrot-top) for the RabbitMQ HTTP API,
|
|
13
|
+
and [ActiveSupport](https://github.com/rails/rails/tree/main/activesupport) from Ruby on Rails.
|
|
14
|
+
|
|
15
|
+
## Target Ruby Version
|
|
16
|
+
|
|
17
|
+
This library targets Ruby 3.0 and later versions
|
|
18
|
+
For JRuby, the supported series are 9.x and 10.x.
|
|
19
|
+
|
|
20
|
+
## Build and Test
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bundle install
|
|
24
|
+
|
|
25
|
+
bundle exec rspec spec
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To run tests use `rspec` directly (not Rake).
|
|
29
|
+
|
|
30
|
+
## Key Files
|
|
31
|
+
|
|
32
|
+
### Core
|
|
33
|
+
|
|
34
|
+
* `lib/hutch.rb`: top-level module, global consumer registry, a connection singleton
|
|
35
|
+
* `lib/hutch/broker.rb`: RabbitMQ connection and channel lifecycle, topology declaration, [delivery acknowledgements](https://www.rabbitmq.com/docs/confirms), [publisher confirms](https://www.rabbitmq.com/docs/confirms), [TLS](https://www.rabbitmq.com/docs/ssl), HTTP API client
|
|
36
|
+
* `lib/hutch/worker.rb`: the main consumer loop: topology and consumer setup, delivery dispatch, error handling
|
|
37
|
+
* `lib/hutch/consumer.rb`: `Hutch::Consumer` module included by consumer classes; DSL: `consume`, `queue_name`, `lazy_queue`, `quorum_queue`, `arguments`, `queue_options`, `serializer`
|
|
38
|
+
* `lib/hutch/publisher.rb`: message serialization, routing, publisher confirms
|
|
39
|
+
* `lib/hutch/message.rb`: message wrapper (delivery_info, properties, payload)
|
|
40
|
+
* `lib/hutch/config.rb`: configuration with 3-tier precedence (defaults < ENV `HUTCH_*` < config file < explicit set)
|
|
41
|
+
* `lib/hutch/cli.rb`: CLI (based on `OptionParser`), Rails app detection, consumer loading, daemonization
|
|
42
|
+
* `lib/hutch/exceptions.rb`: `ConnectionError`, `AuthenticationError`, `WorkerSetupError`, `PublishError`
|
|
43
|
+
* `lib/hutch/logging.rb`: configurable logger with `HutchFormatter`
|
|
44
|
+
* `lib/hutch/waiter.rb`: signal handling (`SIGINT`, `SIGTERM`, `SIGQUIT` for shutdown; `SIGUSR2` for thread dumps)
|
|
45
|
+
* `lib/hutch/version.rb`: the `Hutch::VERSION` constant
|
|
46
|
+
|
|
47
|
+
### Adapters
|
|
48
|
+
|
|
49
|
+
* `lib/hutch/adapter.rb`: runtime adapter selector (Bunny on MRI, March Hare on JRuby)
|
|
50
|
+
* `lib/hutch/adapters/bunny.rb`: Bunny adapter
|
|
51
|
+
* `lib/hutch/adapters/march_hare.rb`: March Hare adapter
|
|
52
|
+
|
|
53
|
+
### Serializers
|
|
54
|
+
|
|
55
|
+
* `lib/hutch/serializers/json.rb`: JSON serialization via `multi_json`
|
|
56
|
+
* `lib/hutch/serializers/identity.rb`: pass-through (no-op) serializer
|
|
57
|
+
|
|
58
|
+
### Error Handlers
|
|
59
|
+
|
|
60
|
+
* `lib/hutch/error_handlers/base.rb`: base class
|
|
61
|
+
* `lib/hutch/error_handlers/logger.rb`: logs errors (default)
|
|
62
|
+
* `lib/hutch/error_handlers/sentry.rb`: sentry-ruby integration
|
|
63
|
+
* `lib/hutch/error_handlers/sentry_raven.rb`: legacy sentry-raven integration
|
|
64
|
+
* `lib/hutch/error_handlers/honeybadger.rb`: Honeybadger
|
|
65
|
+
* `lib/hutch/error_handlers/airbrake.rb`: Airbrake
|
|
66
|
+
* `lib/hutch/error_handlers/rollbar.rb`: Rollbar
|
|
67
|
+
* `lib/hutch/error_handlers/bugsnag.rb`: Bugsnag
|
|
68
|
+
|
|
69
|
+
### Tracers
|
|
70
|
+
|
|
71
|
+
* `lib/hutch/tracers/null_tracer.rb`: no-op (default)
|
|
72
|
+
* `lib/hutch/tracers/newrelic.rb`: NewRelic APM
|
|
73
|
+
* `lib/hutch/tracers/datadog.rb`: Datadog tracing
|
|
74
|
+
|
|
75
|
+
### Acknowledgement Strategies
|
|
76
|
+
|
|
77
|
+
* `lib/hutch/acknowledgements/base.rb`: base interface (a chain of responsibility)
|
|
78
|
+
* `lib/hutch/acknowledgements/nack_on_all_failures.rb`: the default fallback
|
|
79
|
+
|
|
80
|
+
## Test Suite
|
|
81
|
+
|
|
82
|
+
The test suite uses RSpec:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bundle exec rspec spec
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Test files mirror the `lib/hutch/` structure under `spec/hutch/`. Tests are filtered
|
|
89
|
+
by adapter at runtime: Bunny specs are excluded on JRuby, March Hare specs on MRI.
|
|
90
|
+
|
|
91
|
+
## Comments
|
|
92
|
+
|
|
93
|
+
* Only add important comments that express the non-obvious intent, both in tests and in the implementation
|
|
94
|
+
* Keep the comments short
|
|
95
|
+
* Pay attention to the grammar of your comments, including punctuation, full stops, articles, and so on
|
|
96
|
+
|
|
97
|
+
## Change Log
|
|
98
|
+
|
|
99
|
+
If asked to perform change log updates, consult and modify `CHANGELOG.md` and stick to its
|
|
100
|
+
existing writing style.
|
|
101
|
+
|
|
102
|
+
## Releases
|
|
103
|
+
|
|
104
|
+
### How to Roll (Produce) a New Release
|
|
105
|
+
|
|
106
|
+
Suppose the current development version in `CHANGELOG.md` has
|
|
107
|
+
a `## X.Y.0 (in development)` section at the top.
|
|
108
|
+
|
|
109
|
+
To produce a new release:
|
|
110
|
+
|
|
111
|
+
1. Update `CHANGELOG.md`: replace `(in development)` with today's date, e.g. `(Mar 30, 2026)`. Make sure all notable changes since the previous release are listed
|
|
112
|
+
2. Update the version in `lib/hutch/version.rb` to match (remove the `.pre` suffix)
|
|
113
|
+
3. Commit with the message `X.Y.0` (just the version number, nothing else)
|
|
114
|
+
4. Tag the commit: `git tag vX.Y.0`
|
|
115
|
+
5. Bump the dev version: add a new `## X.(Y+1).0 (in development)` section to `CHANGELOG.md` with `No changes yet.` underneath, and update `lib/hutch/version.rb` to the next dev version with a `.pre` suffix
|
|
116
|
+
6. Commit with the message `Bump dev version`
|
|
117
|
+
7. Push: `git push && git push origin vX.(Y+1).0`
|
|
118
|
+
|
|
119
|
+
## Git Instructions
|
|
120
|
+
|
|
121
|
+
* Do not commit changes automatically without an explicit permission to do so
|
|
122
|
+
* Never add yourself to the list of commit co-authors
|
|
123
|
+
* Never mention yourself in commit messages in any way (no "Generated by", no AI tool links, etc)
|
|
124
|
+
|
|
125
|
+
## Style Guide
|
|
126
|
+
|
|
127
|
+
* Never add full stops to Markdown list items
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,51 @@
|
|
|
1
|
-
## 1.
|
|
1
|
+
## 1.4.0 (Apr 7, 2026)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
### Ruby 3.0 is Now Required
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This version adopts [Bunny `3.x`](https://github.com/ruby-amqp/bunny/releases/tag/3.0.0)
|
|
6
|
+
and as a result, requires Ruby 3.0.
|
|
7
|
+
|
|
8
|
+
### `without_namespace` Consumer DSL Option
|
|
9
|
+
|
|
10
|
+
Consumers can now opt out of the automatic namespace prefix
|
|
11
|
+
for their queue name using the `without_namespace` DSL method:
|
|
12
|
+
|
|
13
|
+
``` ruby
|
|
14
|
+
class DeadLetterConsumer
|
|
15
|
+
include Hutch::Consumer
|
|
16
|
+
consume 'dead.letters'
|
|
17
|
+
queue_name 'deadletter'
|
|
18
|
+
without_namespace
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Originally contributed by @tlloydthwaites.
|
|
23
|
+
|
|
24
|
+
GitHub issue: [#393](https://github.com/ruby-amqp/hutch/pull/393)
|
|
25
|
+
|
|
26
|
+
### Consumer Channel Recovery After Delivery Acknowledgement Timeout
|
|
27
|
+
|
|
28
|
+
Hutch now automatically "recovers" (reopens) consumer channels closed by RabbitMQ
|
|
29
|
+
due to a [delivery acknowledgement timeout](https://www.rabbitmq.com/docs/consumers#acknowledgement-timeout).
|
|
30
|
+
|
|
31
|
+
Note that this exception indicates a potential issue on Hutch's end:
|
|
32
|
+
a delivery was not acknowledged in time (30 minutes by default). Even then,
|
|
33
|
+
recovering the channel in this specific scenario is an operational improvement.
|
|
34
|
+
|
|
35
|
+
Contributed by @Garaio-REM.
|
|
36
|
+
|
|
37
|
+
GitHub issue: [#414](https://github.com/ruby-amqp/hutch/pull/414)
|
|
38
|
+
|
|
39
|
+
### Migrated Datadog Tracer From the `ddtrace` Gem to `datadog`
|
|
40
|
+
|
|
41
|
+
The Datadog tracer now uses the `datadog` gem instead of the deprecated `ddtrace`.
|
|
42
|
+
The `ddtrace` gem is still supported but emits a deprecation warning at load time.
|
|
43
|
+
Support for `ddtrace` will be removed in Hutch 2.0.
|
|
44
|
+
|
|
45
|
+
### Deprecated `SentryRaven` Error Handler
|
|
46
|
+
|
|
47
|
+
`Hutch::ErrorHandlers::SentryRaven` now emits a deprecation warning and will be
|
|
48
|
+
removed in Hutch 2.0. Use `Hutch::ErrorHandlers::Sentry` (backed by `sentry-ruby`) instead.
|
|
6
49
|
|
|
7
50
|
### Rails 8.x Compatibility
|
|
8
51
|
|
|
@@ -19,7 +62,7 @@ GitHub issue: [#392](https://github.com/ruby-amqp/hutch/pull/392)
|
|
|
19
62
|
|
|
20
63
|
### Relaxed ActiveSupport Dependency Constraints
|
|
21
64
|
|
|
22
|
-
Contributed by
|
|
65
|
+
Contributed by drobny.
|
|
23
66
|
|
|
24
67
|
GitHub issue: [#402](https://github.com/ruby-amqp/hutch/pull/402)
|
|
25
68
|
|
|
@@ -93,7 +136,7 @@ contains potentially breaking changes.
|
|
|
93
136
|
|
|
94
137
|
This means **some defaults introduced in `0.28.0` ([gocardless/hutch#341](https://github.com/gocardless/hutch/pull/341)) were reverted**.
|
|
95
138
|
The user has to opt in to configure the queue type and mode and other [optional arguments](https://www.rabbitmq.com/queues.html#optional-arguments) they need to use.
|
|
96
|
-
Most optional arguments can be set via [policies](https://www.rabbitmq.com/parameters.html#policies) which is always the recommended approach.
|
|
139
|
+
Most optional arguments can be set via [policies](https://www.rabbitmq.com/parameters.html#policies) which is always the recommended approach.
|
|
97
140
|
Queue type, unfortunately, is not one of them as different queue types have completely different
|
|
98
141
|
implementation details, on disk data formats and so on.
|
|
99
142
|
|
|
@@ -106,7 +149,7 @@ contains potentially breaking changes.
|
|
|
106
149
|
# when in doubt, prefer using a policy to this DSL
|
|
107
150
|
# https://www.rabbitmq.com/parameters.html#policies
|
|
108
151
|
arguments 'x-key': :value
|
|
109
|
-
|
|
152
|
+
|
|
110
153
|
quorum_queue
|
|
111
154
|
end
|
|
112
155
|
```
|
|
@@ -120,7 +163,7 @@ contains potentially breaking changes.
|
|
|
120
163
|
# when in doubt, prefer using a policy to this DSL
|
|
121
164
|
# https://www.rabbitmq.com/parameters.html#policies
|
|
122
165
|
arguments 'x-key': :value
|
|
123
|
-
|
|
166
|
+
|
|
124
167
|
lazy_queue
|
|
125
168
|
classic_queue
|
|
126
169
|
end
|
data/Gemfile
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
|
-
ruby '>=
|
|
3
|
+
ruby '>= 3.0.0'
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ group :development, :test do
|
|
|
23
23
|
gem "sentry-ruby"
|
|
24
24
|
gem "honeybadger"
|
|
25
25
|
gem "newrelic_rpm"
|
|
26
|
-
gem "
|
|
26
|
+
gem "datadog"
|
|
27
27
|
gem "airbrake", "~> 13.0"
|
|
28
28
|
gem "rollbar"
|
|
29
29
|
gem "bugsnag"
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
|
|
3
3
|
CTL=${MARCH_HARE_RABBITMQCTL:="docker exec rabbitmq rabbitmqctl"}
|
|
4
|
-
PLUGINS=${MARCH_HARE_RABBITMQ_PLUGINS:="docker exec rabbitmq rabbitmq-plugins"}
|
|
5
|
-
|
|
6
|
-
$PLUGINS enable rabbitmq_management
|
|
7
|
-
|
|
8
|
-
sleep 3
|
|
9
4
|
|
|
10
5
|
# guest:guest has full access to /
|
|
11
6
|
|
|
@@ -14,7 +9,7 @@ $CTL add_vhost /
|
|
|
14
9
|
$CTL set_permissions -p / guest ".*" ".*" ".*"
|
|
15
10
|
|
|
16
11
|
# Reduce retention policy for faster publishing of stats
|
|
17
|
-
$CTL eval 'supervisor2:terminate_child(rabbit_mgmt_sup_sup, rabbit_mgmt_sup), application:set_env(rabbitmq_management, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_sup_sup:start_child().'
|
|
18
|
-
$CTL eval 'supervisor2:terminate_child(rabbit_mgmt_agent_sup_sup, rabbit_mgmt_agent_sup), application:set_env(rabbitmq_management_agent, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_agent_sup_sup:start_child().'
|
|
12
|
+
$CTL eval 'supervisor2:terminate_child(rabbit_mgmt_sup_sup, rabbit_mgmt_sup), application:set_env(rabbitmq_management, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_sup_sup:start_child().' || true
|
|
13
|
+
$CTL eval 'supervisor2:terminate_child(rabbit_mgmt_agent_sup_sup, rabbit_mgmt_agent_sup), application:set_env(rabbitmq_management_agent, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_agent_sup_sup:start_child().' || true
|
|
19
14
|
|
|
20
15
|
sleep 3
|
data/hutch.gemspec
CHANGED
|
@@ -3,10 +3,10 @@ require File.expand_path('../lib/hutch/version', __FILE__)
|
|
|
3
3
|
Gem::Specification.new do |gem|
|
|
4
4
|
if defined?(JRUBY_VERSION)
|
|
5
5
|
gem.platform = 'java'
|
|
6
|
-
gem.add_runtime_dependency 'march_hare', '>= 4.
|
|
6
|
+
gem.add_runtime_dependency 'march_hare', '>= 4.7.0'
|
|
7
7
|
else
|
|
8
8
|
gem.platform = Gem::Platform::RUBY
|
|
9
|
-
gem.add_runtime_dependency 'bunny', '>=
|
|
9
|
+
gem.add_runtime_dependency 'bunny', '>= 3.1', '< 4.0'
|
|
10
10
|
end
|
|
11
11
|
gem.add_runtime_dependency 'carrot-top', '~> 0.0.7'
|
|
12
12
|
gem.add_runtime_dependency 'multi_json', '~> 1.15'
|
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
|
|
|
16
16
|
gem.summary = 'Opinionated asynchronous inter-service communication using RabbitMQ'
|
|
17
17
|
gem.description = 'Hutch is a Ruby library for enabling asynchronous inter-service communication using RabbitMQ'
|
|
18
18
|
gem.version = Hutch::VERSION.dup
|
|
19
|
-
gem.required_ruby_version = '>=
|
|
19
|
+
gem.required_ruby_version = '>= 3.0'
|
|
20
20
|
gem.authors = ['Harry Marr', 'Michael Klishin']
|
|
21
21
|
gem.homepage = 'https://github.com/ruby-amqp/hutch'
|
|
22
22
|
gem.require_paths = ['lib']
|
data/lib/hutch/adapters/bunny.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Hutch
|
|
|
11
11
|
ConnectionRefused = Bunny::TCPConnectionFailed
|
|
12
12
|
PreconditionFailed = Bunny::PreconditionFailed
|
|
13
13
|
|
|
14
|
-
def_delegators :@connection, :start, :disconnect, :close, :create_channel, :open
|
|
14
|
+
def_delegators :@connection, :start, :disconnect, :close, :create_channel, :open?, :recover_channel_topology
|
|
15
15
|
|
|
16
16
|
def initialize(opts={})
|
|
17
17
|
@connection = Bunny.new(opts)
|
data/lib/hutch/broker.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
require 'active_support/core_ext/object/blank'
|
|
2
2
|
|
|
3
3
|
require 'carrot-top'
|
|
4
|
-
require 'ostruct'
|
|
5
4
|
require 'hutch/logging'
|
|
6
5
|
require 'hutch/exceptions'
|
|
7
6
|
require 'hutch/publisher'
|
|
@@ -114,6 +113,8 @@ module Hutch
|
|
|
114
113
|
logger.info 'enabling publisher confirms'
|
|
115
114
|
ch.confirm_select
|
|
116
115
|
end
|
|
116
|
+
|
|
117
|
+
install_channel_recovery!(ch)
|
|
117
118
|
end
|
|
118
119
|
end
|
|
119
120
|
|
|
@@ -121,6 +122,20 @@ module Hutch
|
|
|
121
122
|
@channel = open_channel
|
|
122
123
|
end
|
|
123
124
|
|
|
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
|
|
137
|
+
end
|
|
138
|
+
|
|
124
139
|
def declare_exchange(ch = channel)
|
|
125
140
|
exchange_name = @config[:mq_exchange]
|
|
126
141
|
exchange_type = @config[:mq_exchange_type]
|
|
@@ -171,15 +186,19 @@ module Hutch
|
|
|
171
186
|
@config[:tracer] && @config[:tracer] != Hutch::Tracers::NullTracer
|
|
172
187
|
end
|
|
173
188
|
|
|
174
|
-
# Create / get a durable queue
|
|
189
|
+
# Create / get a durable queue.
|
|
175
190
|
def queue(name, options = {})
|
|
176
191
|
with_bunny_precondition_handler('queue') do
|
|
177
|
-
|
|
178
|
-
queue_name = namespace.present? ? "#{namespace}:#{name}" : name
|
|
179
|
-
channel.queue(queue_name, **options)
|
|
192
|
+
channel.queue(name, **options)
|
|
180
193
|
end
|
|
181
194
|
end
|
|
182
195
|
|
|
196
|
+
# Apply the configured namespace prefix to a queue name.
|
|
197
|
+
def namespaced_queue_name(name)
|
|
198
|
+
namespace = @config[:namespace].to_s.downcase.gsub(/[^-_:\.\w]/, "")
|
|
199
|
+
namespace.present? ? "#{namespace}:#{name}" : name
|
|
200
|
+
end
|
|
201
|
+
|
|
183
202
|
# Return a mapping of queue names to the routing keys they're bound to.
|
|
184
203
|
def bindings
|
|
185
204
|
results = Hash.new { |hash, key| hash[key] = [] }
|
|
@@ -269,8 +288,11 @@ module Hutch
|
|
|
269
288
|
|
|
270
289
|
private
|
|
271
290
|
|
|
291
|
+
Config = Struct.new(:host, :port, :username, :password, :ssl, :protocol, :sanitized_uri)
|
|
292
|
+
private_constant :Config
|
|
293
|
+
|
|
272
294
|
def api_config
|
|
273
|
-
@api_config ||=
|
|
295
|
+
@api_config ||= Config.new.tap do |config|
|
|
274
296
|
config.host = @config[:mq_api_host]
|
|
275
297
|
config.port = @config[:mq_api_port]
|
|
276
298
|
config.username = @config[:mq_username]
|
data/lib/hutch/consumer.rb
CHANGED
|
@@ -47,6 +47,15 @@ module Hutch
|
|
|
47
47
|
@queue_name = name
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Opt out of the namespace prefix for this consumer's queue
|
|
51
|
+
def without_namespace
|
|
52
|
+
@without_namespace = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def without_namespace?
|
|
56
|
+
!!@without_namespace
|
|
57
|
+
end
|
|
58
|
+
|
|
50
59
|
# Explicitly set the queue mode to 'lazy'
|
|
51
60
|
def lazy_queue
|
|
52
61
|
@queue_mode = 'lazy'
|
|
@@ -10,6 +10,9 @@ module Hutch
|
|
|
10
10
|
unless Raven.respond_to?(:capture_exception)
|
|
11
11
|
raise "The Hutch Sentry error handler requires Raven >= 0.4.0"
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
warn "[DEPRECATION] Hutch::ErrorHandlers::SentryRaven is deprecated and will be removed in Hutch 2.0. " \
|
|
15
|
+
"Use Hutch::ErrorHandlers::Sentry (backed by the sentry-ruby gem) instead." \
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def handle(properties, payload, consumer, ex)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
require '
|
|
1
|
+
begin
|
|
2
|
+
require 'datadog'
|
|
3
|
+
require 'datadog/auto_instrument'
|
|
4
|
+
rescue LoadError
|
|
5
|
+
require 'ddtrace'
|
|
6
|
+
require 'ddtrace/auto_instrument'
|
|
7
|
+
warn "[DEPRECATION] The ddtrace gem is deprecated and Hutch will require the datadog gem in 2.0. " \
|
|
8
|
+
"Please switch to the datadog gem."
|
|
9
|
+
end
|
|
3
10
|
|
|
4
11
|
module Hutch
|
|
5
12
|
module Tracers
|
data/lib/hutch/version.rb
CHANGED
data/lib/hutch/worker.rb
CHANGED
|
@@ -45,9 +45,11 @@ module Hutch
|
|
|
45
45
|
# Bind a consumer's routing keys to its queue, and set up a subscription to
|
|
46
46
|
# receive messages sent to the queue.
|
|
47
47
|
def setup_queue(consumer)
|
|
48
|
-
|
|
48
|
+
queue_name = consumer.get_queue_name
|
|
49
|
+
queue_name = @broker.namespaced_queue_name(queue_name) unless consumer.without_namespace?
|
|
50
|
+
logger.info "setting up queue: #{queue_name}"
|
|
49
51
|
|
|
50
|
-
queue = @broker.queue(
|
|
52
|
+
queue = @broker.queue(queue_name, consumer.get_options)
|
|
51
53
|
@broker.bind_queue(queue, consumer.routing_keys)
|
|
52
54
|
|
|
53
55
|
queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true) do |*args|
|
data/spec/hutch/broker_spec.rb
CHANGED
|
@@ -95,7 +95,7 @@ describe Hutch::Broker do
|
|
|
95
95
|
|
|
96
96
|
context 'when configured with a URI' do
|
|
97
97
|
context 'which specifies the port' do
|
|
98
|
-
before { config[:uri] =
|
|
98
|
+
before { config[:uri] = "amqp://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}:#{config[:mq_port]}/" }
|
|
99
99
|
|
|
100
100
|
it 'successfully connects' do
|
|
101
101
|
c = broker.open_connection
|
|
@@ -105,7 +105,7 @@ describe Hutch::Broker do
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
context 'which does not specify port and uses the amqp scheme' do
|
|
108
|
-
before { config[:uri] =
|
|
108
|
+
before { config[:uri] = "amqp://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}/" }
|
|
109
109
|
|
|
110
110
|
it 'successfully connects' do
|
|
111
111
|
c = broker.open_connection
|
|
@@ -115,7 +115,7 @@ describe Hutch::Broker do
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
context 'which specifies the amqps scheme' do
|
|
118
|
-
before { config[:uri] =
|
|
118
|
+
before { config[:uri] = "amqps://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}/" }
|
|
119
119
|
|
|
120
120
|
it 'utilises TLS' do
|
|
121
121
|
expect(Hutch::Adapter).to receive(:new).with(
|
|
@@ -256,16 +256,30 @@ describe Hutch::Broker do
|
|
|
256
256
|
let(:arguments) { { foo: :bar } }
|
|
257
257
|
before { allow(broker).to receive(:channel) { channel } }
|
|
258
258
|
|
|
259
|
-
it '
|
|
260
|
-
|
|
261
|
-
expect(broker.channel).to receive(:queue) do |*args|
|
|
262
|
-
args.first == ''
|
|
263
|
-
args.last == arguments
|
|
264
|
-
end
|
|
259
|
+
it 'creates a queue with the given name' do
|
|
260
|
+
expect(broker.channel).to receive(:queue).with('test', arguments: arguments)
|
|
265
261
|
broker.queue('test'.freeze, arguments: arguments)
|
|
266
262
|
end
|
|
267
263
|
end
|
|
268
264
|
|
|
265
|
+
describe '#namespaced_queue_name' do
|
|
266
|
+
context 'with a namespace configured' do
|
|
267
|
+
before { config[:namespace] = 'mirror-all.service' }
|
|
268
|
+
|
|
269
|
+
it 'prepends the namespace' do
|
|
270
|
+
expect(broker.namespaced_queue_name('test')).to eq('mirror-all.service:test')
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
context 'without a namespace configured' do
|
|
275
|
+
before { config[:namespace] = nil }
|
|
276
|
+
|
|
277
|
+
it 'returns the name unchanged' do
|
|
278
|
+
expect(broker.namespaced_queue_name('test')).to eq('test')
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
269
283
|
describe '#bindings', rabbitmq: true do
|
|
270
284
|
around { |example| broker.connect { example.run } }
|
|
271
285
|
subject { broker.bindings }
|
data/spec/hutch/consumer_spec.rb
CHANGED
|
@@ -97,6 +97,17 @@ describe Hutch::Consumer do
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
describe '.without_namespace' do
|
|
101
|
+
it 'is false by default' do
|
|
102
|
+
expect(simple_consumer.without_namespace?).to eq(false)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'returns true after calling without_namespace' do
|
|
106
|
+
simple_consumer.without_namespace
|
|
107
|
+
expect(simple_consumer.without_namespace?).to eq(true)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
100
111
|
describe 'default queue mode' do
|
|
101
112
|
it 'does not specify any mode by default' do
|
|
102
113
|
expect(simple_consumer.queue_mode).to eq(nil)
|
|
@@ -14,7 +14,7 @@ describe Hutch::ErrorHandlers::Airbrake do
|
|
|
14
14
|
|
|
15
15
|
it "logs the error to Airbrake" do
|
|
16
16
|
message_id = "1"
|
|
17
|
-
properties =
|
|
17
|
+
properties = Struct.new(:message_id).new(message_id)
|
|
18
18
|
payload = "{}"
|
|
19
19
|
consumer = double
|
|
20
20
|
ex = error
|
|
@@ -23,7 +23,7 @@ describe Hutch::ErrorHandlers::Bugsnag do
|
|
|
23
23
|
|
|
24
24
|
it "logs the error to Bugsnag" do
|
|
25
25
|
message_id = "1"
|
|
26
|
-
properties =
|
|
26
|
+
properties = Struct.new(:message_id).new(message_id)
|
|
27
27
|
payload = "{}"
|
|
28
28
|
consumer = double
|
|
29
29
|
ex = error
|
|
@@ -14,7 +14,7 @@ describe Hutch::ErrorHandlers::Honeybadger do
|
|
|
14
14
|
|
|
15
15
|
it "logs the error to Honeybadger" do
|
|
16
16
|
message_id = "1"
|
|
17
|
-
properties =
|
|
17
|
+
properties = Struct.new(:message_id).new(message_id)
|
|
18
18
|
payload = "{}"
|
|
19
19
|
consumer = double
|
|
20
20
|
ex = error
|
|
@@ -4,7 +4,7 @@ describe Hutch::ErrorHandlers::Logger do
|
|
|
4
4
|
let(:error_handler) { Hutch::ErrorHandlers::Logger.new }
|
|
5
5
|
|
|
6
6
|
describe '#handle' do
|
|
7
|
-
let(:properties) {
|
|
7
|
+
let(:properties) { Struct.new(:message_id).new("1") }
|
|
8
8
|
let(:payload) { "{}" }
|
|
9
9
|
let(:error) { double(message: "Stuff went wrong", class: "RuntimeError",
|
|
10
10
|
backtrace: ["line 1", "line 2"]) }
|
|
@@ -14,7 +14,7 @@ describe Hutch::ErrorHandlers::Rollbar do
|
|
|
14
14
|
|
|
15
15
|
it "logs the error to Rollbar" do
|
|
16
16
|
message_id = "1"
|
|
17
|
-
properties =
|
|
17
|
+
properties = Struct.new(:message_id).new(message_id)
|
|
18
18
|
payload = "{}"
|
|
19
19
|
consumer = double
|
|
20
20
|
ex = error
|
|
@@ -4,7 +4,7 @@ describe Hutch::ErrorHandlers::SentryRaven do
|
|
|
4
4
|
let(:error_handler) { Hutch::ErrorHandlers::SentryRaven.new }
|
|
5
5
|
|
|
6
6
|
describe '#handle' do
|
|
7
|
-
let(:properties) {
|
|
7
|
+
let(:properties) { Struct.new(:message_id).new("1") }
|
|
8
8
|
let(:payload) { "{}" }
|
|
9
9
|
let(:error) do
|
|
10
10
|
begin
|
data/spec/hutch/worker_spec.rb
CHANGED
|
@@ -4,7 +4,8 @@ require 'hutch/worker'
|
|
|
4
4
|
describe Hutch::Worker do
|
|
5
5
|
let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
|
|
6
6
|
get_queue_name: 'consumer', get_arguments: {},
|
|
7
|
-
get_options: {}, get_serializer: nil
|
|
7
|
+
get_options: {}, get_serializer: nil,
|
|
8
|
+
without_namespace?: false) }
|
|
8
9
|
let(:consumers) { [consumer, double('Consumer')] }
|
|
9
10
|
let(:broker) { Hutch::Broker.new }
|
|
10
11
|
let(:setup_procs) { Array.new(2) { Proc.new {} } }
|
|
@@ -32,13 +33,27 @@ describe Hutch::Worker do
|
|
|
32
33
|
|
|
33
34
|
describe '#setup_queue' do
|
|
34
35
|
let(:queue) { double('Queue', bind: nil, subscribe: nil) }
|
|
35
|
-
before { allow(broker).to receive_messages(queue: queue, bind_queue: nil) }
|
|
36
|
+
before { allow(broker).to receive_messages(queue: queue, bind_queue: nil, namespaced_queue_name: 'consumer') }
|
|
36
37
|
|
|
37
|
-
it 'creates a queue' do
|
|
38
|
-
expect(broker).to receive(:
|
|
38
|
+
it 'creates a queue with a namespaced name' do
|
|
39
|
+
expect(broker).to receive(:namespaced_queue_name).with('consumer').and_return('ns:consumer')
|
|
40
|
+
expect(broker).to receive(:queue).with('ns:consumer', consumer.get_options).and_return(queue)
|
|
39
41
|
worker.setup_queue(consumer)
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
context 'when the consumer uses without_namespace' do
|
|
45
|
+
let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
|
|
46
|
+
get_queue_name: 'consumer', get_arguments: {},
|
|
47
|
+
get_options: {}, get_serializer: nil,
|
|
48
|
+
without_namespace?: true) }
|
|
49
|
+
|
|
50
|
+
it 'creates a queue without applying the namespace' do
|
|
51
|
+
expect(broker).not_to receive(:namespaced_queue_name)
|
|
52
|
+
expect(broker).to receive(:queue).with('consumer', consumer.get_options).and_return(queue)
|
|
53
|
+
worker.setup_queue(consumer)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
42
57
|
it 'binds the queue to each of the routing keys' do
|
|
43
58
|
expect(broker).to receive(:bind_queue).with(queue, %w( a b c ))
|
|
44
59
|
worker.setup_queue(consumer)
|
|
@@ -61,6 +76,7 @@ describe Hutch::Worker do
|
|
|
61
76
|
context 'with a configured consumer tag prefix that is too long' do
|
|
62
77
|
let(:maximum_size) { 255 - SecureRandom.uuid.size - 1 }
|
|
63
78
|
before { Hutch::Config.set(:consumer_tag_prefix, 'a'.*(maximum_size + 1)) }
|
|
79
|
+
after { Hutch::Config.set(:consumer_tag_prefix, 'hutch') }
|
|
64
80
|
|
|
65
81
|
it 'raises an error' do
|
|
66
82
|
expect { worker.setup_queue(consumer) }.to raise_error(/Tag must be 255 bytes long at most/)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'hutch/broker'
|
|
3
|
+
require 'hutch/worker'
|
|
4
|
+
require 'hutch/consumer'
|
|
5
|
+
require 'bunny'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'timeout'
|
|
9
|
+
|
|
10
|
+
describe 'channel recovery after delivery acknowledgement timeout', rabbitmq: true, adapter: :bunny do
|
|
11
|
+
let(:log) { StringIO.new }
|
|
12
|
+
let(:logger) { Logger.new(log) }
|
|
13
|
+
let(:exchange_name) { "hutch.integration.exchange.#{SecureRandom.hex(4)}" }
|
|
14
|
+
let(:queue_name) { "hutch.integration.queue.#{SecureRandom.hex(4)}" }
|
|
15
|
+
let(:routing_key) { "hutch.integration.key.#{SecureRandom.hex(4)}" }
|
|
16
|
+
|
|
17
|
+
let(:processed) { [] }
|
|
18
|
+
let(:processed_lock) { Mutex.new }
|
|
19
|
+
let(:timed_out_once) { [false] }
|
|
20
|
+
|
|
21
|
+
let(:consumer_class) do
|
|
22
|
+
msgs = processed
|
|
23
|
+
lock = processed_lock
|
|
24
|
+
rk = routing_key
|
|
25
|
+
qn = queue_name
|
|
26
|
+
timed_out = timed_out_once
|
|
27
|
+
|
|
28
|
+
Class.new do
|
|
29
|
+
include Hutch::Consumer
|
|
30
|
+
|
|
31
|
+
consume rk
|
|
32
|
+
queue_name qn
|
|
33
|
+
arguments(
|
|
34
|
+
'x-queue-type' => 'quorum',
|
|
35
|
+
'x-consumer-timeout' => 60_000
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
define_method(:process) do |message|
|
|
39
|
+
if message['id'] == 'trigger-timeout' && !timed_out[0]
|
|
40
|
+
timed_out[0] = true
|
|
41
|
+
sleep 210
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
lock.synchronize { msgs << message['id'] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
let(:broker) { Hutch::Broker.new }
|
|
50
|
+
let(:worker) { Hutch::Worker.new(broker, [consumer_class], []) }
|
|
51
|
+
|
|
52
|
+
let(:publisher) do
|
|
53
|
+
Bunny.new(
|
|
54
|
+
host: Hutch::Config[:mq_host],
|
|
55
|
+
port: Hutch::Config[:mq_port],
|
|
56
|
+
username: Hutch::Config[:mq_username],
|
|
57
|
+
password: Hutch::Config[:mq_password],
|
|
58
|
+
vhost: Hutch::Config[:mq_vhost]
|
|
59
|
+
).tap(&:start)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
let(:publisher_channel) { publisher.create_channel }
|
|
63
|
+
let(:exchange) { publisher_channel.topic(exchange_name, durable: true) }
|
|
64
|
+
|
|
65
|
+
before do
|
|
66
|
+
Hutch::Logging.logger = logger
|
|
67
|
+
Hutch::Config.set(:mq_exchange, exchange_name)
|
|
68
|
+
Hutch::Config.set(:force_publisher_confirms, false)
|
|
69
|
+
Hutch::Config.set(:client_logger, logger)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
after do
|
|
73
|
+
publisher_channel.close rescue nil
|
|
74
|
+
publisher.close rescue nil
|
|
75
|
+
broker.disconnect rescue nil
|
|
76
|
+
Hutch::Logging.logger = Logger.new(File::NULL)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def wait_for(timeout, label)
|
|
80
|
+
Timeout.timeout(timeout) do
|
|
81
|
+
loop do
|
|
82
|
+
return true if yield
|
|
83
|
+
sleep 0.25
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
rescue Timeout::Error
|
|
87
|
+
raise <<~MSG
|
|
88
|
+
Timed out waiting for: #{label}
|
|
89
|
+
|
|
90
|
+
processed_messages=#{processed_messages.inspect}
|
|
91
|
+
channel_open=#{broker.channel.open? rescue 'unknown'}
|
|
92
|
+
channel_closed=#{broker.channel.closed? rescue 'unknown'}
|
|
93
|
+
|
|
94
|
+
log output:
|
|
95
|
+
#{log_output}
|
|
96
|
+
MSG
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def processed_messages
|
|
100
|
+
processed_lock.synchronize { processed.dup }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def log_output
|
|
104
|
+
log.rewind
|
|
105
|
+
log.read
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def publish_message(id)
|
|
109
|
+
exchange.publish(
|
|
110
|
+
JSON.dump('id' => id),
|
|
111
|
+
routing_key: routing_key,
|
|
112
|
+
content_type: 'application/json',
|
|
113
|
+
persistent: true
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# This spec is intentionally slow because RabbitMQ enforces delivery
|
|
118
|
+
# acknowledgement timeouts on a periodic sweep, not immediately at the deadline.
|
|
119
|
+
it 're-subscribes and consumes later messages after RabbitMQ closes the channel for ack timeout' do
|
|
120
|
+
broker.connect
|
|
121
|
+
worker.setup_queues
|
|
122
|
+
|
|
123
|
+
publish_message('trigger-timeout')
|
|
124
|
+
|
|
125
|
+
wait_for(240, 'delivery acknowledgement timeout') do
|
|
126
|
+
log_output.match?(/delivery acknowledgement on channel \d+ timed out/i)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
publish_message('after-recovery')
|
|
130
|
+
|
|
131
|
+
wait_for(90, 'after-recovery message consumption') do
|
|
132
|
+
processed_messages.include?('after-recovery')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
expect(log_output).to match(/delivery acknowledgement on channel \d+ timed out/i)
|
|
136
|
+
expect(log_output).to match(/recovered consumer channel after a delivery acknowledgement timeout/i)
|
|
137
|
+
expect(processed_messages).to include('after-recovery')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'hutch/broker'
|
|
3
|
+
require 'hutch/worker'
|
|
4
|
+
require 'hutch/consumer'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
describe 'publishing and consuming messages', rabbitmq: true, adapter: :bunny do
|
|
9
|
+
let(:exchange_name) { "hutch.test.#{SecureRandom.hex(4)}" }
|
|
10
|
+
let(:routing_key) { "test.message" }
|
|
11
|
+
let(:received) { [] }
|
|
12
|
+
|
|
13
|
+
let(:consumer_class) do
|
|
14
|
+
msgs = received
|
|
15
|
+
rk = routing_key
|
|
16
|
+
qn = "test_consumer_#{SecureRandom.hex(4)}"
|
|
17
|
+
|
|
18
|
+
Class.new do
|
|
19
|
+
include Hutch::Consumer
|
|
20
|
+
consume rk
|
|
21
|
+
queue_name qn
|
|
22
|
+
|
|
23
|
+
define_method(:process) { |message| msgs << message.body }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:broker) { Hutch::Broker.new }
|
|
28
|
+
let(:worker) { Hutch::Worker.new(broker, [consumer_class], []) }
|
|
29
|
+
|
|
30
|
+
before do
|
|
31
|
+
Hutch::Config.set(:mq_exchange, exchange_name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
after do
|
|
35
|
+
broker.disconnect rescue nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'publishes and consumes a message' do
|
|
39
|
+
broker.connect
|
|
40
|
+
worker.setup_queues
|
|
41
|
+
|
|
42
|
+
broker.publish(routing_key, { test: 'data' })
|
|
43
|
+
|
|
44
|
+
Timeout.timeout(5) { sleep 0.1 until received.any? }
|
|
45
|
+
|
|
46
|
+
expect(received.first).to eq('test' => 'data')
|
|
47
|
+
end
|
|
48
|
+
end
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hutch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harry Marr
|
|
8
8
|
- Michael Klishin
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: bunny
|
|
@@ -17,20 +16,20 @@ dependencies:
|
|
|
17
16
|
requirements:
|
|
18
17
|
- - ">="
|
|
19
18
|
- !ruby/object:Gem::Version
|
|
20
|
-
version: '
|
|
19
|
+
version: '3.1'
|
|
21
20
|
- - "<"
|
|
22
21
|
- !ruby/object:Gem::Version
|
|
23
|
-
version: '
|
|
22
|
+
version: '4.0'
|
|
24
23
|
type: :runtime
|
|
25
24
|
prerelease: false
|
|
26
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
27
26
|
requirements:
|
|
28
27
|
- - ">="
|
|
29
28
|
- !ruby/object:Gem::Version
|
|
30
|
-
version: '
|
|
29
|
+
version: '3.1'
|
|
31
30
|
- - "<"
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
32
|
+
version: '4.0'
|
|
34
33
|
- !ruby/object:Gem::Dependency
|
|
35
34
|
name: carrot-top
|
|
36
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,7 +74,6 @@ dependencies:
|
|
|
75
74
|
version: '4.2'
|
|
76
75
|
description: Hutch is a Ruby library for enabling asynchronous inter-service communication
|
|
77
76
|
using RabbitMQ
|
|
78
|
-
email:
|
|
79
77
|
executables:
|
|
80
78
|
- hutch
|
|
81
79
|
extensions: []
|
|
@@ -85,6 +83,7 @@ files:
|
|
|
85
83
|
- ".gitignore"
|
|
86
84
|
- ".rspec"
|
|
87
85
|
- ".yardopts"
|
|
86
|
+
- AGENTS.md
|
|
88
87
|
- CHANGELOG.md
|
|
89
88
|
- Gemfile
|
|
90
89
|
- Guardfile
|
|
@@ -150,6 +149,8 @@ files:
|
|
|
150
149
|
- spec/hutch/waiter_spec.rb
|
|
151
150
|
- spec/hutch/worker_spec.rb
|
|
152
151
|
- spec/hutch_spec.rb
|
|
152
|
+
- spec/integration/channel_recovery_spec.rb
|
|
153
|
+
- spec/integration/publish_consume_spec.rb
|
|
153
154
|
- spec/spec_helper.rb
|
|
154
155
|
- templates/default/class/html/settings.erb
|
|
155
156
|
- templates/default/class/setup.rb
|
|
@@ -164,7 +165,6 @@ homepage: https://github.com/ruby-amqp/hutch
|
|
|
164
165
|
licenses:
|
|
165
166
|
- MIT
|
|
166
167
|
metadata: {}
|
|
167
|
-
post_install_message:
|
|
168
168
|
rdoc_options: []
|
|
169
169
|
require_paths:
|
|
170
170
|
- lib
|
|
@@ -172,15 +172,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
172
172
|
requirements:
|
|
173
173
|
- - ">="
|
|
174
174
|
- !ruby/object:Gem::Version
|
|
175
|
-
version: '
|
|
175
|
+
version: '3.0'
|
|
176
176
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
177
|
requirements:
|
|
178
178
|
- - ">="
|
|
179
179
|
- !ruby/object:Gem::Version
|
|
180
180
|
version: '0'
|
|
181
181
|
requirements: []
|
|
182
|
-
rubygems_version: 3.
|
|
183
|
-
signing_key:
|
|
182
|
+
rubygems_version: 3.6.2
|
|
184
183
|
specification_version: 4
|
|
185
184
|
summary: Opinionated asynchronous inter-service communication using RabbitMQ
|
|
186
185
|
test_files:
|
|
@@ -202,4 +201,6 @@ test_files:
|
|
|
202
201
|
- spec/hutch/waiter_spec.rb
|
|
203
202
|
- spec/hutch/worker_spec.rb
|
|
204
203
|
- spec/hutch_spec.rb
|
|
204
|
+
- spec/integration/channel_recovery_spec.rb
|
|
205
|
+
- spec/integration/publish_consume_spec.rb
|
|
205
206
|
- spec/spec_helper.rb
|