1 | # Messaging API supporting acknowledgements and request-response
|
2 |
|
3 | [![Build Status](http://ci.salemove.com/buildStatus/icon?job=freddy)](http://ci.salemove.com/job/freddy/)
|
4 | [![Code Climate](https://codeclimate.com/repos/52a1f75613d6374c030432d2/badges/f8f96e50aa9f57dfae00/gpa.png)](https://codeclimate.com/repos/52a1f75613d6374c030432d2/feed)
|
5 |
|
6 | ## Usage
|
7 |
|
8 | ### Ruby
|
9 |
|
10 | #### Setup
|
11 |
|
12 | * Inject the appropriate default logger and set up connection parameters:
|
13 |
|
14 | ```ruby
|
15 | Freddy.setup(Logger.new(STDOUT), host: 'localhost', port: 5672, user: 'guest', pass: 'guest')
|
16 | ```
|
17 |
|
18 | * Use Freddy to deliver and respond to messages:
|
19 |
|
20 | ```ruby
|
21 | freddy = Freddy.new(logger = Freddy.logger)
|
22 | ```
|
23 |
|
24 | * by default the Freddy instance will reuse connections and queues for messaging, if you want to use a distinct tcp connection, response queue and timeout checking thread, then use
|
25 |
|
26 | ```ruby
|
27 | freddy.use_distinct_connection
|
28 | ```
|
29 |
|
30 | #### Destinations
|
31 | Freddy encourages but doesn't enforce the following protocol for destinations:
|
32 |
|
33 | * For sending messages to services:
|
34 |
|
35 | ```
|
36 | <service_name>.<method_name>.<anything_else_you_need>.<...>
|
37 | ```
|
38 |
|
39 | * For reporting errors:
|
40 |
|
41 | ```
|
42 | <service_name>.<method_name>.'responder'|'producer'.'errors'
|
43 | ```
|
44 |
|
45 | #### Delivering messages
|
46 |
|
47 | * Simply deliver a message:
|
48 | ```ruby
|
49 | freddy.deliver(destination, message)
|
50 | ```
|
51 | * destination is the recipient of the message
|
52 | * message is the contents of the message
|
53 |
|
54 | * Deliver a message expecting explicit acknowledgement
|
55 | ```ruby
|
56 | freddy.deliver_with_ack(destination, message, timeout_seconds = 3) do |error|
|
57 | ```
|
58 |
|
59 | * If timeout_seconds pass without a response from the responder, then the callback is called with a timeout error.
|
60 |
|
61 | * callback is called with one argument: a string that contains an error message if
|
62 | * the message couldn't be sent to any responders or
|
63 | * the responder negatively acknowledged(nacked) the message or
|
64 | * the responder finished working but didn't positively acknowledge the message
|
65 |
|
66 | * callback is called with one argument that is nil if the responder positively acknowledged the message
|
67 | * note that the callback will not be called in the case that there is a responder who receives the message, but the responder doesn't finish processing the message or dies in the process.
|
68 |
|
69 | * Deliver expecting a response
|
70 | ```ruby
|
71 | freddy.deliver_with_response(destination, message, timeout_seconds = 3) do |response, msg_handler|
|
72 | ```
|
73 |
|
74 | * If `timeout_seconds pass` without a response from the responder then the callback is called with the hash
|
75 | ```ruby
|
76 | { error: 'Timed out waiting for response' }
|
77 | ```
|
78 |
|
79 | * Callback is called with 2 arguments
|
80 |
|
81 | * The parsed response
|
82 |
|
83 | * The `MessageHandler`(described further down)
|
84 |
|
85 | * Synchronous deliver expecting response
|
86 | ```ruby
|
87 | response = freddy.deliver_with_response(destination, message, timeout_seconds = 3)
|
88 | ```
|
89 |
|
90 | #### Responding to messages
|
91 |
|
92 | * Respond to messages while not blocking the current thread:
|
93 | ```ruby
|
94 | freddy.respond_to destination do |message, msg_handler|
|
95 | ```
|
96 | * Respond to message and block the thread
|
97 | ```ruby
|
98 | freddy.respond_to_and_block destination do |message, msg_handler|
|
99 | ```
|
100 |
|
101 | * The callback is called with 2 arguments
|
102 |
|
103 | * the parsed message (note that in the message all keys are symbolized)
|
104 | * the `MessageHandler` (described further down)
|
105 |
|
106 | #### The MessageHandler
|
107 |
|
108 | When responding to messages the MessageHandler is given as the second argument.
|
109 | ```ruby
|
110 | freddy.respond_to destination do |message, msg_handler|
|
111 | ```
|
112 |
|
113 | The following operations are supported:
|
114 |
|
115 | * acknowledging the message
|
116 | ```ruby
|
117 | msg_handler.ack(response = nil)
|
118 | ```
|
119 |
|
120 | * when the message was produced with `produce_with_response`, then the response is sent to the original producer
|
121 |
|
122 | * when the message was produced with `produce_with_ack`, then only a positive acknowledgement is sent, the provided response is dicarded
|
123 |
|
124 | * negatively acknowledging the message
|
125 | ```ruby
|
126 | msg_handler.nack(error = "Couldn't process message")
|
127 | ```
|
128 |
|
129 | * when the message was produced with `produce_with_response`, then the following hash is sent to the original producer
|
130 | ```ruby
|
131 | { error: error }
|
132 | ```
|
133 |
|
134 | * when the message was produced with `produce_with_ack`, then the error (e.g negative acknowledgement) is sent to the original producer
|
135 |
|
136 | * Getting additional properties of the message (shouldn't be necessary under normal circumstances)
|
137 | ```ruby
|
138 | msg_handler.properties
|
139 | ```
|
140 |
|
141 | #### Tapping into messages
|
142 | When it's necessary to receive messages but not consume them, consider tapping.
|
143 |
|
144 | ```ruby
|
145 | freddy.tap_into pattern do |message, destination|
|
146 | ```
|
147 |
|
148 | * `destination` refers to the destination that the message was sent to
|
149 | * Note that it is not possible to acknowledge or respond to message while tapping.
|
150 | * When tapping the following wildcards are supported in the `pattern` :
|
151 | * `#` matching 0 or more words
|
152 | * `*` matching exactly one word
|
153 |
|
154 | Examples:
|
155 |
|
156 | ```ruby
|
157 | freddy.tap_into "i.#.free"
|
158 | ```
|
159 |
|
160 | receives messages that are delivered to `"i.want.to.break.free"`
|
161 |
|
162 | ```ruby
|
163 | freddy.tap_into "somebody.*.love"
|
164 | ```
|
165 |
|
166 | receives messages that are delivered to `somebody.to.love` but doesn't receive messages delivered to `someboy.not.to.love`
|
167 |
|
168 | It is also possible to use the blocking version of `tap_into`:
|
169 |
|
170 | ```ruby
|
171 | freddy.tap_into_and_block pattern, &callback do |message, destination|
|
172 | ```
|
173 |
|
174 | #### The ResponderHandler
|
175 |
|
176 | When responding to a message or tapping the ResponderHandler is returned.
|
177 | ```ruby
|
178 | responder_handler = freddy.respond_to ....
|
179 | ```
|
180 |
|
181 | The following operations are supported:
|
182 |
|
183 | * stop responding
|
184 | ```ruby
|
185 | responder_handler.cancel
|
186 | ```
|
187 |
|
188 | * join the current thread to the responder thread
|
189 | ```ruby
|
190 | responder_handler.join
|
191 | ```
|
192 |
|
193 | * delete the destination
|
194 | ```ruby
|
195 | responder_handler.destroy_destination
|
196 | ```
|
197 |
|
198 | * Primary use case is in tests to not leave dangling destinations. It deletes the destination even if there are responders for the same destination in other parts of the system. Use with caution in production code.
|
199 |
|
200 |
|
201 | #### Notes about concurrency
|
202 |
|
203 | The underlying bunny implementation uses 1 responder thread by default. This means that if there is a time-consuming process or a sleep call in a responder then other responders will not receive messages concurrently.
|
204 |
|
205 | This is especially devious when using `deliver_with_response` in a responder because `deliver_with_response` creates a new anonymous responder which will not receive the response if the parent responder uses a sleep call.
|
206 |
|
207 | To resolve this problem *freddy* uses 4 responder threads by default (configurable by `responder_thread_count`). Note that this means that ordered message processing is not guaranteed by default. Read more from <http://rubybunny.info/articles/concurrency.html>.
|
208 |
|
209 | ***
|
210 |
|
211 | ### Node.js
|
212 |
|
213 | #### Setup
|
214 | ```coffee
|
215 | Freddy = require 'freddy'
|
216 | Freddy.addErrorListener(listener)
|
217 | Freddy.connect('amqp://guest:guest@localhost:5672', logger).then (freddy) ->
|
218 | continueWith(freddy)
|
219 | , (error) ->
|
220 | doSthWithError(error)
|
221 | ```
|
222 |
|
223 | #### Delivering messages
|
224 | ```coffee
|
225 | freddy.deliver(destination, message, options = {})
|
226 |
|
227 | freddy.deliverWithAck(destination, message, callback)
|
228 |
|
229 | freddy.deliverWithResponse(destination, message, callback)
|
230 | ```
|
231 |
|
232 | * The previous 2 can be used with additional options also:
|
233 | ```coffee
|
234 | freddy.deliverWithAckAndOptions(destination, message, options, callback)
|
235 |
|
236 | freddy.deliverWithResponseAndOptions(destination, message, options, callback)
|
237 | ```
|
238 |
|
239 | The options include:
|
240 |
|
241 | * `timeout`: In seconds, defaults to 3.
|
242 | * `suppressLog`: Avoid logging the message contents
|
243 |
|
244 |
|
245 | #### Responding to messages
|
246 | ```coffee
|
247 | freddy.respondTo(destination, callback)
|
248 | ```
|
249 |
|
250 | * `respondTo` returns a promise which resolved with the ResponderHandler
|
251 |
|
252 | ```coffee
|
253 | freddy.respondTo(destination, callback)
|
254 | .then (responderHandler) ->
|
255 | doSthWith(responderHandler.cancel())
|
256 | ```
|
257 |
|
258 | #### The MessageHandler
|
259 | No differences to ruby spec
|
260 |
|
261 | #### Tapping into messages
|
262 |
|
263 | ```coffee
|
264 | responderHandler = freddy.tapInto(pattern, callback)
|
265 | ```
|
266 |
|
267 | No other differences to ruby spec, blocking variant is not provided for obvious reasons.
|
268 |
|
269 | #### The ResponderHandler
|
270 |
|
271 | * When cancelling the responder returns a promise, no messages will be received after the promise resolves.
|
272 |
|
273 | ```coffee
|
274 | freddy.respondTo(destination, (->)).then (responderHandler) ->
|
275 | responderHandler.cancel().then ->
|
276 | freddy.deliver(destination, easy: 'go') #will not be received
|
277 | ```
|
278 | * The join method is not provided for obvious reasons.
|
279 |
|
280 | ## Development
|
281 |
|
282 | * Use RSpec and mocha, make sure the tests pass.
|
283 | * Don't leak underlying messaging protocol internals.
|
284 |
|
285 | ## Credits
|
286 |
|
287 | **freddy** was originally written by [Urmas Talimaa] as part of SaleMove development team.
|
288 |
|
289 | ![SaleMove Inc. 2012][SaleMove Logo]
|
290 |
|
291 | **freddy** is maintained and funded by [SaleMove, Inc].
|
292 |
|
293 | The names and logos for **SaleMove** are trademarks of SaleMove, Inc.
|
294 |
|
295 | ## License
|
296 |
|
297 | **freddy** is Copyright © 2013 SaleMove Inc. It is free software, and may be redistributed under the terms specified in the [Apache License].
|
298 |
|
299 | [Urmas Talimaa]: https://github.com/urmastalimaa?source=c "Urmas"
|
300 | [SaleMove, Inc]: http://salemove.com/ "SaleMove Website"
|
301 | [SaleMove Logo]: http://app.salemove.com/assets/logo.png "SaleMove Inc. 2012"
|
302 | [Apache License]: http://choosealicense.com/licenses/apache/ "Apache License"
|