nebulous_stomp 1.1.5

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.
@@ -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
+