dkastner-hutch 0.17.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +397 -0
- data/Gemfile +22 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +315 -0
- data/Rakefile +14 -0
- data/bin/hutch +8 -0
- data/circle.yml +3 -0
- data/examples/consumer.rb +13 -0
- data/examples/producer.rb +10 -0
- data/hutch.gemspec +24 -0
- data/lib/hutch/broker.rb +356 -0
- data/lib/hutch/cli.rb +205 -0
- data/lib/hutch/config.rb +121 -0
- data/lib/hutch/consumer.rb +66 -0
- data/lib/hutch/error_handlers/airbrake.rb +26 -0
- data/lib/hutch/error_handlers/honeybadger.rb +28 -0
- data/lib/hutch/error_handlers/logger.rb +16 -0
- data/lib/hutch/error_handlers/sentry.rb +23 -0
- data/lib/hutch/error_handlers.rb +8 -0
- data/lib/hutch/exceptions.rb +7 -0
- data/lib/hutch/logging.rb +32 -0
- data/lib/hutch/message.rb +33 -0
- data/lib/hutch/tracers/newrelic.rb +19 -0
- data/lib/hutch/tracers/null_tracer.rb +15 -0
- data/lib/hutch/tracers.rb +6 -0
- data/lib/hutch/version.rb +4 -0
- data/lib/hutch/worker.rb +110 -0
- data/lib/hutch.rb +60 -0
- data/spec/hutch/broker_spec.rb +325 -0
- data/spec/hutch/cli_spec.rb +80 -0
- data/spec/hutch/config_spec.rb +126 -0
- data/spec/hutch/consumer_spec.rb +130 -0
- data/spec/hutch/error_handlers/airbrake_spec.rb +34 -0
- data/spec/hutch/error_handlers/honeybadger_spec.rb +36 -0
- data/spec/hutch/error_handlers/logger_spec.rb +15 -0
- data/spec/hutch/error_handlers/sentry_spec.rb +20 -0
- data/spec/hutch/logger_spec.rb +28 -0
- data/spec/hutch/message_spec.rb +38 -0
- data/spec/hutch/worker_spec.rb +98 -0
- data/spec/hutch_spec.rb +87 -0
- data/spec/spec_helper.rb +35 -0
- metadata +187 -0
data/README.md
ADDED
@@ -0,0 +1,315 @@
|
|
1
|
+
![](http://cl.ly/image/3h0q3F3G142K/hutch.png)
|
2
|
+
|
3
|
+
Hutch is a Ruby library for enabling asynchronous inter-service communication
|
4
|
+
in a service-oriented architecture, using RabbitMQ.
|
5
|
+
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/hutch.png)](http://badge.fury.io/rb/hutch)
|
7
|
+
[![Build Status](https://travis-ci.org/gocardless/hutch.png?branch=master)](https://travis-ci.org/gocardless/hutch)
|
8
|
+
[![Dependency Status](https://gemnasium.com/gocardless/hutch.png)](https://gemnasium.com/gocardless/hutch)
|
9
|
+
[![Code Climate](https://codeclimate.com/github/gocardless/hutch.png)](https://codeclimate.com/github/gocardless/hutch)
|
10
|
+
|
11
|
+
To install with RubyGems:
|
12
|
+
|
13
|
+
```
|
14
|
+
$ gem install hutch
|
15
|
+
```
|
16
|
+
|
17
|
+
## Project Maturity
|
18
|
+
|
19
|
+
Hutch is a moderately mature project (started in early 2013)
|
20
|
+
that was extracted from production systems.
|
21
|
+
|
22
|
+
|
23
|
+
## Overview
|
24
|
+
|
25
|
+
Hutch is a conventions-based framework for writing services that communicate
|
26
|
+
over RabbitMQ. Hutch is opinionated: it uses topic exchanges for message
|
27
|
+
distribution and makes some assumptions about how consumers and publishers
|
28
|
+
should work.
|
29
|
+
|
30
|
+
With Hutch, consumers are stored in separate files and include the `Hutch::Consumer` module.
|
31
|
+
They are then loaded by a command line runner which connects to RabbitMQ, sets up queues and bindings,
|
32
|
+
and so on. Publishers connect to RabbitMQ via `Hutch.connect` and publish using `Hutch.publish`.
|
33
|
+
|
34
|
+
Hutch uses [Bunny](http://rubybunny.info) under the hood.
|
35
|
+
|
36
|
+
|
37
|
+
## Defining Consumers
|
38
|
+
|
39
|
+
Consumers receive messages from a RabbitMQ queue. That queue may be bound to
|
40
|
+
one or more topics (represented by routing keys).
|
41
|
+
|
42
|
+
To create a consumer, include the `Hutch::Consumer` module in a class that
|
43
|
+
defines a `#process` method. `#process` should take a single argument, which
|
44
|
+
will be a `Message` object. The `Message` object encapsulates the message data,
|
45
|
+
along with any associated metadata. To access properties of the message, use
|
46
|
+
Hash-style indexing syntax:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
message[:id] # => "02ABCXYZ"
|
50
|
+
```
|
51
|
+
|
52
|
+
To subscribe to a topic, pass a routing key to `consume` in the class
|
53
|
+
definition. To bind to multiple routing keys, simply pass extra routing keys
|
54
|
+
in as additional arguments. Refer to the [RabbitMQ docs on topic exchanges
|
55
|
+
][topic-docs] for more information about how to use routing keys. Here's an
|
56
|
+
example consumer:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class FailedPaymentConsumer
|
60
|
+
include Hutch::Consumer
|
61
|
+
consume 'gc.ps.payment.failed'
|
62
|
+
|
63
|
+
def process(message)
|
64
|
+
mark_payment_as_failed(message[:id])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
By default, the queue name will be named using the consumer class. You can set
|
70
|
+
the queue name explicitly by using the `queue_name` method:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class FailedPaymentConsumer
|
74
|
+
include Hutch::Consumer
|
75
|
+
consume 'gc.ps.payment.failed'
|
76
|
+
queue_name 'failed_payments'
|
77
|
+
|
78
|
+
def process(message)
|
79
|
+
mark_payment_as_failed(message[:id])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
You can also set custom arguments per consumer. This example declares a consumer with
|
85
|
+
a maximum length of 10 messages:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class FailedPaymentConsumer
|
89
|
+
include Hutch::Consumer
|
90
|
+
consume 'gc.ps.payment.failed'
|
91
|
+
arguments 'x-max-length' => 10
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
Custom queue arguments can be found on [this page](https://www.rabbitmq.com/extensions.html).
|
96
|
+
|
97
|
+
Consumers can write to Hutch's log by calling the logger method. The logger method returns
|
98
|
+
a [Logger object](http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html).
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class FailedPaymentConsumer
|
102
|
+
include Hutch::Consumer
|
103
|
+
consume 'gc.ps.payment.failed'
|
104
|
+
|
105
|
+
def process(message)
|
106
|
+
logger.info "Marking payment #{message[:id]} as failed"
|
107
|
+
mark_payment_as_failed(message[:id])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
If you are using Hutch with Rails and want to make Hutch log to the Rails
|
113
|
+
logger rather than `stdout`, add this to `config/initializers/hutch.rb`
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Hutch::Logging.logger = Rails.logger
|
117
|
+
```
|
118
|
+
|
119
|
+
A logger can be set for the client by adding this config before calling `Hutch.connect`
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
client_logger = Logger.new("/path/to/bunny.log")
|
123
|
+
Hutch::Config.set(:client_logger, client_logger)
|
124
|
+
```
|
125
|
+
|
126
|
+
See this [RabbitMQ tutorial on topic exchanges](http://www.rabbitmq.com/tutorials/tutorial-five-ruby.html)
|
127
|
+
to learn more.
|
128
|
+
|
129
|
+
### Message Processing Tracers
|
130
|
+
|
131
|
+
Tracers allow you to track message processing.
|
132
|
+
|
133
|
+
#### NewRelic
|
134
|
+
```ruby
|
135
|
+
Hutch::Config.set(:tracer, Hutch::Tracers::NewRelic)
|
136
|
+
```
|
137
|
+
This will enable NewRelic custom instrumentation. Batteries included! Screenshoots available [here](https://monosnap.com/list/557020a000779174f23467e3).
|
138
|
+
|
139
|
+
## Running Hutch
|
140
|
+
|
141
|
+
After installing the Hutch gem, you should be able to start it by simply
|
142
|
+
running `hutch` on the command line. `hutch` takes a number of options:
|
143
|
+
|
144
|
+
```console
|
145
|
+
$ hutch -h
|
146
|
+
usage: hutch [options]
|
147
|
+
--mq-host HOST Set the RabbitMQ host
|
148
|
+
--mq-port PORT Set the RabbitMQ port
|
149
|
+
-t, --[no-]mq-tls Use TLS for the AMQP connection
|
150
|
+
--mq-tls-cert FILE Certificate for TLS client verification
|
151
|
+
--mq-tls-key FILE Private key for TLS client verification
|
152
|
+
--mq-exchange EXCHANGE Set the RabbitMQ exchange
|
153
|
+
--mq-vhost VHOST Set the RabbitMQ vhost
|
154
|
+
--mq-username USERNAME Set the RabbitMQ username
|
155
|
+
--mq-password PASSWORD Set the RabbitMQ password
|
156
|
+
--mq-api-host HOST Set the RabbitMQ API host
|
157
|
+
--mq-api-port PORT Set the RabbitMQ API port
|
158
|
+
-s, --[no-]mq-api-ssl Use SSL for the RabbitMQ API
|
159
|
+
--config FILE Load Hutch configuration from a file
|
160
|
+
--require PATH Require a Rails app or path
|
161
|
+
--[no-]autoload-rails Require the current rails app directory
|
162
|
+
-q, --quiet Quiet logging
|
163
|
+
-v, --verbose Verbose logging
|
164
|
+
--version Print the version and exit
|
165
|
+
-h, --help Show this message and exit
|
166
|
+
```
|
167
|
+
|
168
|
+
The first three are for configuring which RabbitMQ instance to connect to.
|
169
|
+
`--require` is covered in the next section. Configurations can also be
|
170
|
+
specified in a YAML file for convenience by passing the file location
|
171
|
+
to the --config option. The file should look like:
|
172
|
+
|
173
|
+
```yaml
|
174
|
+
mq_username: peter
|
175
|
+
mq_password: rabbit
|
176
|
+
mq_host: broker.yourhost.com
|
177
|
+
```
|
178
|
+
|
179
|
+
Passing a setting as a command-line option will overwrite what's specified
|
180
|
+
in the config file, allowing for easy customization.
|
181
|
+
|
182
|
+
### Loading Consumers
|
183
|
+
|
184
|
+
Using Hutch with a Rails app is simple. Either start Hutch in the working
|
185
|
+
directory of a Rails app, or pass the path to a Rails app in with the
|
186
|
+
`--require` option. Consumers defined in Rails apps should be placed with in
|
187
|
+
the `app/consumers/` directory, to allow them to be auto-loaded when Rails
|
188
|
+
boots.
|
189
|
+
|
190
|
+
To require files that define consumers manually, simply pass each file as an
|
191
|
+
option to `--require`. Hutch will automatically detect whether you've provided
|
192
|
+
a Rails app or a standard file, and take the appropriate behaviour:
|
193
|
+
|
194
|
+
```bash
|
195
|
+
$ hutch --require path/to/rails-app # loads a rails app
|
196
|
+
$ hutch --require path/to/file.rb # loads a ruby file
|
197
|
+
```
|
198
|
+
|
199
|
+
### Stopping Hutch
|
200
|
+
|
201
|
+
Hutch supports graceful stops. That means that if done correctly, Hutch will wait for your consumer to finish processing before exiting.
|
202
|
+
|
203
|
+
To gracefully stop your workers, you may send the following signals to your Hutch processes: `INT`, `TERM`, or `QUIT`.
|
204
|
+
|
205
|
+
```bash
|
206
|
+
kill -SIGINT 123 # or kill -2 123
|
207
|
+
kill -SIGTERM 456 # or kill -15 456
|
208
|
+
kill -SIGQUIT 789 # or kill -3 789
|
209
|
+
```
|
210
|
+
|
211
|
+
![](http://g.recordit.co/wyCdzG9Kh3.gif)
|
212
|
+
|
213
|
+
## Producers
|
214
|
+
|
215
|
+
Hutch includes a `publish` method for sending messages to Hutch consumers. When
|
216
|
+
possible, this should be used, rather than directly interfacing with RabbitMQ
|
217
|
+
libraries.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
Hutch.connect
|
221
|
+
Hutch.publish('routing.key', subject: 'payment', action: 'received')
|
222
|
+
```
|
223
|
+
|
224
|
+
### Producer Configuration
|
225
|
+
|
226
|
+
Producers are not run with the 'hutch' command. You can specify configuration
|
227
|
+
options as follows:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
Hutch::Config.set(:mq_exchange, 'name')
|
231
|
+
```
|
232
|
+
|
233
|
+
### Publisher Confirms
|
234
|
+
|
235
|
+
For maximum message reliability when producing messages, you can force Hutch to use
|
236
|
+
[Publisher Confirms](https://www.rabbitmq.com/confirms.html) and wait for a confirmation
|
237
|
+
after every message published. This is the safest possible option for publishers
|
238
|
+
but also results in a **significant throughput drop**.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
Hutch::Config.set(:force_publisher_confirms, true)
|
242
|
+
```
|
243
|
+
|
244
|
+
### Writing Well-Behaved Publishers
|
245
|
+
|
246
|
+
You may need to send messages to Hutch from languages other than Ruby. This
|
247
|
+
prevents the use of `Hutch.publish`, requiring custom publication code to be
|
248
|
+
written. There are a few things to keep in mind when writing producers that
|
249
|
+
send messages to Hutch.
|
250
|
+
|
251
|
+
- Make sure that the producer exchange name matches the exchange name that
|
252
|
+
Hutch is using.
|
253
|
+
- Hutch works with topic exchanges, check the producer is also using topic
|
254
|
+
exchanges.
|
255
|
+
- Use message routing keys that match those used in your Hutch consumers.
|
256
|
+
- Be sure your exchanges are marked as durable. In the Ruby AMQP gem, this is
|
257
|
+
done by passing `durable: true` to the exchange creation method.
|
258
|
+
- Publish messages as persistent.
|
259
|
+
- Using publisher confirms is highly recommended.
|
260
|
+
|
261
|
+
Here's an example of a well-behaved publisher, minus publisher confirms:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
AMQP.connect(host: config[:host]) do |connection|
|
265
|
+
channel = AMQP::Channel.new(connection)
|
266
|
+
exchange = channel.topic(config[:exchange], durable: true)
|
267
|
+
|
268
|
+
message = JSON.dump({ subject: 'Test', id: 'abc' })
|
269
|
+
exchange.publish(message, routing_key: 'test', persistent: true)
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
If using publisher confirms with amqp gem, see [this issue][pc-issue]
|
274
|
+
and [this gist][pc-gist] for more info.
|
275
|
+
|
276
|
+
## Configuration Reference
|
277
|
+
|
278
|
+
### Config File
|
279
|
+
|
280
|
+
It is recommended to use a separate config file, unless you use URIs for connection (see below).
|
281
|
+
|
282
|
+
Known configuration parameters are:
|
283
|
+
|
284
|
+
* `mq_host`: RabbitMQ hostname (default: `localhost`)
|
285
|
+
* `mq_port`: RabbitMQ port (default: `5672`)
|
286
|
+
* `mq_vhost`: vhost to use (default: `/`)
|
287
|
+
* `mq_username`: username to use (default: `guest`, only can connect from localhost as of RabbitMQ 3.3.0)
|
288
|
+
* `mq_password`: password to use (default: `guest`)
|
289
|
+
* `mq_tls`: should TLS be used? (default: `false`)
|
290
|
+
* `mq_tls_cert`: path to client TLS certificate (public key)
|
291
|
+
* `mq_tls_key`: path to client TLS private key
|
292
|
+
* `require_paths`: array of paths to require
|
293
|
+
* `autoload_rails`: should Hutch command line runner try to automatically load Rails environment files?
|
294
|
+
* `daemonise`: should Hutch runner process daemonise?
|
295
|
+
* `pidfile`: path to PID file the runner should use
|
296
|
+
* `channel_prefetch`: basic.qos prefetch value to use (default: `0`, no limit). See Bunny and RabbitMQ documentation.
|
297
|
+
* `publisher_confirms`: enables publisher confirms. Leaves it up to the app how they are
|
298
|
+
tracked (e.g. using `Hutch::Broker#confirm_select` callback or `Hutch::Broker#wait_for_confirms`)
|
299
|
+
* `force_publisher_confirms`: enables publisher confirms, forces `Hutch::Broker#wait_for_confirms` for every publish. **This is the safest option which also offers the lowest throughput**.
|
300
|
+
* `log_level`: log level used by the standard Ruby logger (default: `Logger::INFO`)
|
301
|
+
* `mq_exchange`: exchange to use for publishing (default: `hutch`)
|
302
|
+
* `heartbeat`: [RabbitMQ heartbeat timeout](http://rabbitmq.com/heartbeats.html) (default: `30`)
|
303
|
+
* `connection_timeout`: Bunny's socket open timeout (default: `11`)
|
304
|
+
* `read_timeout`: Bunny's socket read timeout (default: `11`)
|
305
|
+
* `write_timemout`: Bunny's socket write timeout (default: `11`)
|
306
|
+
* `tracer`: tracer to use to track message processing
|
307
|
+
|
308
|
+
|
309
|
+
## Supported RabbitMQ Versions
|
310
|
+
|
311
|
+
Hutch requires RabbitMQ 3.3 or later.
|
312
|
+
|
313
|
+
---
|
314
|
+
|
315
|
+
GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/jobs/backend_developer).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
desc "Run an IRB session with Hutch pre-loaded"
|
4
|
+
task :console do
|
5
|
+
exec "irb -I lib -r hutch"
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Run the test suite"
|
9
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
10
|
+
t.pattern = FileList['spec/**/*_spec.rb']
|
11
|
+
t.rspec_opts = %w(--color --format doc)
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => :spec
|
data/bin/hutch
ADDED
data/circle.yml
ADDED
data/hutch.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path('../lib/hutch/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.add_runtime_dependency 'bunny', '>= 1.7.0'
|
5
|
+
gem.add_runtime_dependency 'carrot-top', '~> 0.0.7'
|
6
|
+
gem.add_runtime_dependency 'multi_json', '~> 1.5'
|
7
|
+
gem.add_runtime_dependency 'activesupport', '>= 3.0'
|
8
|
+
gem.add_development_dependency 'rspec', '~> 3.0'
|
9
|
+
gem.add_development_dependency 'simplecov', '~> 0.7.1'
|
10
|
+
|
11
|
+
gem.name = 'hutch'
|
12
|
+
gem.summary = 'Easy inter-service communication using RabbitMQ.'
|
13
|
+
gem.description = 'Hutch is a Ruby library for enabling asynchronous ' +
|
14
|
+
'inter-service communication using RabbitMQ.'
|
15
|
+
gem.version = Hutch::VERSION.dup
|
16
|
+
gem.authors = ['Harry Marr']
|
17
|
+
gem.email = ['developers@gocardless.com']
|
18
|
+
gem.homepage = 'https://github.com/gocardless/hutch'
|
19
|
+
gem.require_paths = ['lib']
|
20
|
+
gem.license = 'MIT'
|
21
|
+
gem.executables = ['hutch']
|
22
|
+
gem.files = `git ls-files`.split("\n")
|
23
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
24
|
+
end
|