nebulous_stomp 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,193 @@
1
+ Nebulous: The Protocol
2
+ ======================
3
+
4
+ (It was going to be "The Nebulous Protocol", but that sounds like something with
5
+ Matt Damon in it.)
6
+
7
+
8
+ Introduction
9
+ ------------
10
+ Project Nebulous is a STOMP client written in ABL, and also the protocol that
11
+ allows our different systems, ABL or not, to pass messages. This document,
12
+ obviously, concentrates on the protocol.
13
+
14
+ The basic idea is this: one system passes a message, a _request_; and then
15
+ waits for an answer to that message, a _response_, from another system. STOMP
16
+ doesn't explicitly support that; the protocol does it.
17
+
18
+ (Note that a request and a response are just notional terms; you might in
19
+ theory have a request which provokes a response which provokes another response
20
+ in return, in which case the second message is both a response and a request,
21
+ and the terms get rather less useful.)
22
+
23
+
24
+ The Protocol
25
+ ------------
26
+ Let's start with the actual protocol, the rules. They won't make sense without
27
+ reading the rest of the document, but:
28
+
29
+ 1. Every valid request (that is, every message with an identifiable verb other
30
+ than "success" or "error") will always generate a response. If the handling
31
+ routine cannot generate a valid response, or if there is no routine to handle
32
+ the verb in question, then the response should be the error verb. If no
33
+ response is required, you should still receive the success verb.
34
+
35
+ 2. You should expect that requests that do not have a verb will not be responded
36
+ to or even understood. (However, there is no general requirement for message
37
+ bodies to follow the verb / parameters / description format. If you have a
38
+ program that sends a request and waits for a response, the format of that
39
+ response is outside the remit of The Protocol.)
40
+
41
+ 3. If a request has the _neb-reply-to_ header, the response should use that as
42
+ a destination; otherwise it should be sent to the same destination that the
43
+ request was sent to.
44
+
45
+ 4. If the request has a _neb-reply-id_ header, then the response should set the
46
+ _neb-in-reply-to_ header to that.
47
+
48
+ 5. The request may specify the content type of a response (JSON or text) by
49
+ it's own content type. That is, if a response can be in either form, it
50
+ should take the form of the request as an indication as which to use.
51
+
52
+ 6. A given queue that is the target of requests within The Protocol will be for
53
+ only one system or common group of systems. They may consume (ACK) messages
54
+ sent to it even if they do not have the facility to process them. If
55
+ multiple systems share a queue, they should understand that messages will be
56
+ consumed from it at random.
57
+
58
+
59
+ Components
60
+ ----------
61
+ By way of an explanation of the above section.
62
+
63
+ ### Headers ###
64
+
65
+ STOMP allows you to define custom headers. We have three.
66
+
67
+ * _neb-reply-to_ is may be set on a request to the destination that any
68
+ response should be posted to.
69
+
70
+ * _neb-reply-id_ may be set on a request to an arbitrary string to help
71
+ you identify the response.
72
+
73
+ * _neb-in-reply-to_ is set on a response and contains the nebulous-reply-id
74
+ of the request.
75
+
76
+ ### Message Body ###
77
+
78
+ The Protocol specifies a format for the message body. It consists of three
79
+ fields:
80
+
81
+ * _verb_ is the keyword that tells the receiving system how to process the
82
+ message.
83
+
84
+ * _parameters_ is an arbitrary field that is context-dependant on verb. The
85
+ routine that handles the verb is expected to be able to parse it. It is
86
+ optional.
87
+
88
+ * _description_ is a text field and can contain anything. It is optional.
89
+
90
+ Nebulous supports message bodies in either JSON (in which case parameters is an
91
+ array) or plain text (in which case it expects to find the fields formatted in
92
+ the same way as STOMP headers: seperated by line breaks, where each line
93
+ consists of the field name, a colon, and the value).
94
+
95
+ There are a couple of special verbs:
96
+
97
+ * _success_ as a verb in a response is used when no information needs to be
98
+ returned except that the request operation went ahead without problem.
99
+
100
+ * _error_ as a verb is used when the requested operation failed. The
101
+ expectation is that you will put an error message in the description field;
102
+ you can use parameters as you see fit, of course.
103
+
104
+
105
+ Notes on usage
106
+ --------------
107
+ There are two use cases here. The first (let's call it _Q & A_) is when a
108
+ process needs information and sends a request, then waits for a response. The
109
+ second (let's call it _Responder_) is at the other end of that process; it
110
+ camps onto one or more queues waiting for requests, and arranges for responses
111
+ to be sent.
112
+
113
+ ### Responder ###
114
+
115
+ Let's talk about the Responder use case first, since it's simpler; we've
116
+ basically been talking about it for the whole of this document. You'll need to
117
+ designate a queue for incoming requests on any given system. You might want
118
+ more than one; in ABL, where traffic jams are likely because I can't just
119
+ spawn up a thread to handle each incoming message, my current thinking is to
120
+ have two incoming queues, one for reqests that take a few seconds, and another
121
+ for requests that take longer.
122
+
123
+ Remember that rule 6 says that any messages that go to a queue like this will
124
+ be consumed without concern for whether the message will make sense to the
125
+ system in question; this is basically there so that the ABL code can split up
126
+ the process of grabbing new messages from the process of working out what they
127
+ are and how to answer them.
128
+
129
+ But the simplicity is appealing, regardless: post to this queue and the
130
+ Responder that looks after this system will process it. Rule 1 guarantees
131
+ you an answer so long as the system is up, so if you don't get one then either
132
+ the target system is down or it's too busy to respond.
133
+
134
+ The expectation is that the Responder system should use the verb of the message
135
+ to control how it is dealt with. The parameters field is verb-specific; the
136
+ combination of verb, expected parameters, and the nature of the returned
137
+ message form a sort of contract, ie "verb x always expects parameters like
138
+ this, and always behaves like that in response". This is partly implied by Rule
139
+ 2, I think.
140
+
141
+ ### Q & A ###
142
+
143
+ The Q & A use case is more interesting since it's what the whole thing is for,
144
+ but some of it really falls outside of The Protocol.
145
+
146
+ In theory, Rule 3 says you can fail to use _neb-reply-to_ and pick up your
147
+ response from the same queue you posted the message to. But rule 6 says that if
148
+ you do that you don't have any guarantee at all of getting your message; the
149
+ Responder will take it. So for practical purposes you almost always *have* to
150
+ set a reply-to queue in your request.
151
+
152
+ Likewise, Rule 4 says that _neb-reply-id_ is optional. But in practice you
153
+ should almost certainly set it. Yes, you can specify a brand new queue that is
154
+ unique to your request -- or probably unique, anyway -- but it turns out that
155
+ some message servers, our RabbitMQ included, don't let you subscribe to a queue
156
+ that doesn't exist. It's easy enough to create a queue: you just send a message
157
+ to it. But now there are two messages in that queue (or more if turns out that
158
+ you've got some queue namespace collision after all) and you have to pick your
159
+ response from it, and the easiest way is to set a reply id.
160
+
161
+ So let's assume a median worst case: you're posting a request to a Responder
162
+ queue with a reply-id set to something hopefully unique, and reply-to set to a
163
+ common queue that many processes use to pick up replies. There are two
164
+ challenges here: first, working out which message is yours; second, avoiding
165
+ consuming those messages that are not.
166
+
167
+ For a unique reply-id you could do worse than starting with the session ID that
168
+ STOMP returns when you send a CONNECT frame; clearly the message server thinks
169
+ that is unique, and it should know. In the Ruby Stomp gem, you can get it with
170
+ `client.connection_frame().headers["session"]` where client is your
171
+ `Stomp::Client` instance; in my ABL jhstomp.p library call `get_session_id()`.
172
+
173
+ Now that you have a good reply ID you can tell which is yours by Rule 4; just
174
+ test the _neb-in-reply-to_ header of each message.
175
+
176
+ The second problem, of avoiding consuming messages that don't match your
177
+ reply-id, is handled by careful use of STOMP. If when subscribing you set the
178
+ header `ack:client-individual`, then you must manually acknowledge each message
179
+ you want to consume with an ACK frame.
180
+
181
+ Finally, you get to handle the response. Rule 1 says that it will either be an
182
+ error verb, a success verb ... or something else specific to the verb. The
183
+ nature of messages in responses is really outside of The Protocol; you are free
184
+ to use the verb / parameters / description format if you wish. Again, I'm
185
+ assuming that a given verb will always require the same parameters and return
186
+ the same message. I think that to do otherwise would be very confusing. But The
187
+ Protocol can't enforce that.
188
+
189
+ Note also that while The Protocol says that a request should always result in a
190
+ response, there is nothing to say that the sender of the request should care --
191
+ say, in the example of a request that results in a report being emailed, which
192
+ takes 20 minutes.
193
+
data/nebulous.gemspec ADDED
@@ -0,0 +1,46 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'nebulous/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "nebulous_stomp"
7
+ spec.version = Nebulous::VERSION
8
+ spec.authors = ["Andy Jones"]
9
+ spec.email = ["andy.jones@twosticksconsulting.co.uk"]
10
+ spec.summary = %q{Handles request-and-response messaging via STOMP}
11
+ spec.description = <<~DESC
12
+ A library and protocol to allow disperate systems to ask a question via
13
+ STOMP and receive an answer in return. Optionally, answers can be cached in
14
+ Redis.
15
+ DESC
16
+
17
+ spec.license = "MIT"
18
+ spec.homepage = "https://bitbucket.org/andy-twosticks/nebulous"
19
+
20
+ spec.files = `hg status -macn0`.split("\x0")
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.extra_rdoc_files = spec.files.grep(%r{^md/})
26
+
27
+ spec.requirements << 'STOMP Messaging server'
28
+ spec.requirements << 'Redis server (optional)'
29
+
30
+ spec.post_install_message = <<~MESSAGE
31
+ Nebulous has been installed ...sort of... ::waves arms noncomittedly::
32
+ MESSAGE
33
+
34
+ spec.add_development_dependency "bundler", "~> 1.11"
35
+ spec.add_development_dependency "rake", "~> 10.5"
36
+ spec.add_development_dependency "rspec", "~> 3.4"
37
+ spec.add_development_dependency "rdoc"
38
+ spec.add_development_dependency "pry"
39
+ spec.add_development_dependency "pry-doc"
40
+ spec.add_development_dependency "ripper-tags"
41
+
42
+ spec.add_runtime_dependency 'stomp', '>=1.3'
43
+ spec.add_runtime_dependency 'redis', '>=3.1'
44
+ spec.add_runtime_dependency 'devnull', '~>0.1'
45
+
46
+ end
@@ -0,0 +1,5 @@
1
+ class DocNoPending < RSpec::Core::Formatters::DocumentationFormatter
2
+ RSpec::Core::Formatters.register self, :example_pending
3
+
4
+ def example_pending(notifications); end
5
+ end
data/spec/helpers.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Helpers
2
+
3
+ def stomp_message(contentType, body, inReplyTo=nil)
4
+
5
+ headers = { 'destination' => '/queue/foo',
6
+ 'message-id' => '999',
7
+ 'content-type' => contentType }
8
+
9
+ headers['neb-in-reply-to'] = inReplyTo if inReplyTo
10
+
11
+ mess = ['MESSAGE'] \
12
+ + headers.map{|k,v| "#{k}:#{v}" } \
13
+ << '' \
14
+ << body
15
+
16
+ Stomp::Message.new( mess.join("\n") + "\0" )
17
+ end
18
+ ##
19
+
20
+
21
+ end
22
+