nebulous_stomp 2.0.2 → 3.0.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.hgignore +2 -0
  3. data/.hgtags +1 -0
  4. data/README.md +225 -28
  5. data/feature/connection_example.yaml +24 -0
  6. data/feature/feature_test_spec.rb +247 -0
  7. data/feature/gimme.rb +91 -0
  8. data/lib/nebulous_stomp/listener.rb +107 -0
  9. data/lib/nebulous_stomp/message.rb +132 -265
  10. data/lib/nebulous_stomp/msg/body.rb +169 -0
  11. data/lib/nebulous_stomp/msg/header.rb +98 -0
  12. data/lib/nebulous_stomp/param.rb +16 -35
  13. data/lib/nebulous_stomp/redis_handler.rb +19 -29
  14. data/lib/nebulous_stomp/redis_handler_null.rb +12 -11
  15. data/lib/nebulous_stomp/redis_helper.rb +110 -0
  16. data/lib/nebulous_stomp/request.rb +212 -0
  17. data/lib/nebulous_stomp/stomp_handler.rb +30 -96
  18. data/lib/nebulous_stomp/stomp_handler_null.rb +8 -22
  19. data/lib/nebulous_stomp/target.rb +52 -0
  20. data/lib/nebulous_stomp/version.rb +1 -1
  21. data/lib/nebulous_stomp.rb +63 -50
  22. data/md/LICENSE.txt +20 -2
  23. data/md/nebulous_protocol.md +25 -18
  24. data/spec/listener_spec.rb +104 -0
  25. data/spec/message_spec.rb +227 -116
  26. data/spec/nebulous_spec.rb +44 -9
  27. data/spec/param_spec.rb +16 -33
  28. data/spec/redis_handler_null_spec.rb +0 -2
  29. data/spec/redis_handler_spec.rb +0 -2
  30. data/spec/redis_helper_spec.rb +107 -0
  31. data/spec/request_spec.rb +249 -0
  32. data/spec/stomp_handler_null_spec.rb +33 -34
  33. data/spec/stomp_handler_spec.rb +1 -74
  34. data/spec/target_spec.rb +97 -0
  35. metadata +20 -11
  36. data/lib/nebulous_stomp/nebrequest.rb +0 -259
  37. data/lib/nebulous_stomp/nebrequest_null.rb +0 -37
  38. data/spec/nebrequest_null_spec.rb +0 -219
  39. data/spec/nebrequest_spec.rb +0 -239
  40. data/spec/through_test_spec.rb +0 -80
@@ -0,0 +1,212 @@
1
+ require_relative 'stomp_handler'
2
+ require_relative 'redis_handler'
3
+ require_relative 'message'
4
+ require_relative 'target'
5
+
6
+
7
+ module NebulousStomp
8
+
9
+
10
+ ##
11
+ # Class to handle a request which returns a Message; the Question-Answer use case.
12
+ #
13
+ # message = NebulousStomp::Message.new(verb: "ping")
14
+ # request = NebulousStomp::Request.new(:target1, message)
15
+ # response1 = request.send
16
+ #
17
+ # This replaces the old NebRequest class; it's much more clearly a wrapper for a Message, now.
18
+ #
19
+ class Request
20
+
21
+ # The Target object we are sending the request to
22
+ attr_reader :target
23
+
24
+ # The message we are sending (might not be the Message object you passed...)
25
+ attr_reader :message
26
+
27
+ # If you are testing you can write these with a test object like StompHandlerNull for example
28
+ attr_writer :stomp_handler, :redis_handler
29
+
30
+ ##
31
+ # :call-seq:
32
+ # Request.new(target, message)
33
+ #
34
+ # Pass either a Target or a target name; and a Message (which has a verb)
35
+ #
36
+ def initialize(target, message)
37
+ @target = parse_target(target)
38
+ @message = parse_message(message, @target)
39
+ NebulousStomp.logger.debug(__FILE__) { "New Request for verb #{@message.verb}" }
40
+
41
+ # Get a connection to StompHandler ASAP and set reply_id on @message
42
+ ensure_stomp_connected if NebulousStomp.on?
43
+ end
44
+
45
+ ##
46
+ # :call-seq:
47
+ # request.send_no_cache -> (Message)
48
+ # request.send_no_cache(mtimeout) -> (Message)
49
+ #
50
+ # Send a request and return the response, without using the cache.
51
+ #
52
+ # Parameters:
53
+ # * mTimeout -- Message timout in seconds; defaults to #message_timeout
54
+ #
55
+ # Raises ArgumentError, NebulousTimeout or NebulousError as necessary.
56
+ #
57
+ # Note that this routine completely ignores Redis. It doesn't just not check the cache; it also
58
+ # doesn't update it.
59
+ #
60
+ def send_no_cache(mtimeout=message_timeout)
61
+ return nil unless NebulousStomp.on?
62
+ NebulousStomp.logger.info(__FILE__) { "Sending request to target #{@target.name}" }
63
+
64
+ ensure_stomp_connected
65
+ neb_qna(mtimeout)
66
+ ensure
67
+ stomp_handler.stomp_disconnect
68
+ end
69
+
70
+ ##
71
+ # :call-seq:
72
+ # request.send -> (Message)
73
+ # request.send(mTImeout) -> (Message)
74
+ # request.send(mtimeout,ctimeout) -> (Message)
75
+ #
76
+ # As send_nocache, but without not using the cache :)
77
+ #
78
+ # Parameters:
79
+ # * mtimeout -- Message timout in seconds; defaults to @mTimeout
80
+ # * ctimeout -- Cache timout in seconds; defaults to @cTimeout
81
+ #
82
+ # Raises ArgumentError, NebulousTimeout, NebulousError as necessary.
83
+ #
84
+ def send(mtimeout=message_timeout, ctimeout=cache_timeout)
85
+ return nil unless NebulousStomp.on?
86
+ return send_no_cache(mtimeout) unless NebulousStomp.redis_on?
87
+ ensure_redis_connected
88
+
89
+ if (mess = cache_read).nil?
90
+ mess = send_no_cache(mtimeout)
91
+ cache_write(mess, ctimeout)
92
+ end
93
+
94
+ mess
95
+ ensure
96
+ redis_handler.quit
97
+ end
98
+
99
+ ##
100
+ # :call-seq:
101
+ # request.clear_cache -> self
102
+ #
103
+ # Clear the cache of responses to this request - just this request.
104
+ #
105
+ def clear_cache
106
+ return self unless NebulousStomp.redis_on?
107
+ ensure_redis_connected
108
+ redis_handler.del(@message.protocol_json)
109
+ self
110
+ ensure
111
+ redis_handler.quit
112
+ end
113
+
114
+ ##
115
+ # Returns the default message timeout
116
+ #
117
+ def message_timeout
118
+ @target.message_timeout || Param.get(:messageTimeout)
119
+ end
120
+
121
+ ##
122
+ # Returns the default cache timeout
123
+ #
124
+ def cache_timeout
125
+ Param.get(:cacheTimeout)
126
+ end
127
+
128
+ private
129
+
130
+ def stomp_handler
131
+ @stomp_handler ||= StompHandler.new(Param.get :stompConnectHash)
132
+ end
133
+
134
+ def redis_handler
135
+ @redis_handler ||= RedisHandler.new(Param.get :redisConnectHash)
136
+ end
137
+
138
+ ##
139
+ # Helper routine for initialize
140
+ #
141
+ def parse_message(message, target)
142
+ fail ArgumentError, "Message was not a Message" unless message.is_a? Message
143
+ fail ArgumentError, "Message does not have a verb" unless message.verb
144
+
145
+ new_message = ->(h){ Message.new(message.to_h.merge h) }
146
+ message.reply_to ? message : new_message.(replyTo: target.send_queue)
147
+ end
148
+
149
+ ##
150
+ # Helper routine for initialize
151
+ #
152
+ def parse_target(target)
153
+ t = target.is_a?(Target) ? target : Param.get_target(target)
154
+ fail ArgumentError, "Target was not a Target or a target name" unless t
155
+ t
156
+ end
157
+
158
+ ##
159
+ # Connect to Stomp
160
+ # If we've lost the connection then reconnect but *keep replyID*
161
+ #
162
+ def ensure_stomp_connected
163
+ stomp_handler.stomp_connect unless stomp_handler.connected?
164
+ @message.reply_id = stomp_handler.calc_reply_id if @message.reply_id.nil?
165
+ end
166
+
167
+ ##
168
+ # Connect to Redis
169
+ #
170
+ def ensure_redis_connected
171
+ redis_handler.connect unless redis_handler.connected?
172
+ end
173
+
174
+ ##
175
+ # Send a message via STOMP and wait for a response
176
+ #
177
+ def neb_qna(mTimeout)
178
+ stomp_handler.send_message(@target.receive_queue, @message)
179
+
180
+ response = nil
181
+ stomp_handler.listen_with_timeout(@target.send_queue, mTimeout) do |msg|
182
+ if @message.reply_id && msg.in_reply_to != @message.reply_id
183
+ false
184
+ else
185
+ response = msg
186
+ true
187
+ end
188
+ end
189
+
190
+ response
191
+ end
192
+
193
+ ##
194
+ # Read from the Redis cache
195
+ #
196
+ def cache_read
197
+ found = redis_handler.get(@message.protocol_json)
198
+ found.nil? ? nil : Message.from_cache(found)
199
+ end
200
+
201
+ ##
202
+ # Write to the Redis cache
203
+ #
204
+ def cache_write(response, timeout)
205
+ redis_handler.set(@message.protocol_json, response.to_h, ex: timeout)
206
+ end
207
+
208
+ end # Request
209
+
210
+
211
+ end
212
+
@@ -8,9 +8,9 @@ module NebulousStomp
8
8
  ##
9
9
  # A Class to deal with talking to STOMP via the Stomp gem.
10
10
  #
11
- # You will need to instantiate this yourself if you only want to listen for messages. But if you
12
- # want to send a request and receive a response, you should never need this -- a NebRequest
13
- # returns a Message.
11
+ # You shouldn't ever need to instantiate this yourself. For listening to messages and
12
+ # responding, use NebulousStomp::Listener. For sending a message and waiting for a response, you
13
+ # want NebulousStomp::Request (passing it a NebulousStomp::Message).
14
14
  #
15
15
  class StompHandler
16
16
 
@@ -22,51 +22,6 @@ module NebulousStomp
22
22
  #
23
23
  class << self
24
24
 
25
-
26
- ##
27
- # Parse stomp headers & body and return body as something Ruby-ish.
28
- # It might not be a hash, in fact -- it could be an array of hashes.
29
- #
30
- # We assume that you are getting this from a STOMP message; the routine
31
- # might not work if it is passed something other than Stomp::Message
32
- # headers.
33
- #
34
- # If you have better intelligence as to the content type of the message,
35
- # pass the content type as the optional third parameter.
36
- #
37
- def body_to_hash(headers, body, contentType=nil)
38
- hdrs = headers || {}
39
-
40
- raise ArgumentError, "headers is not a hash" \
41
- unless hdrs.kind_of? Hash
42
-
43
- type = contentType \
44
- || hdrs["content-type"] || hdrs[:content_type] \
45
- || hdrs["contentType"] || hdrs[:contentType]
46
-
47
- hash = nil
48
-
49
- if type =~ /json$/i
50
- begin
51
- hash = JSON.parse(body)
52
- rescue JSON::ParserError, TypeError
53
- hash = {}
54
- end
55
-
56
- else
57
- # We assume that text looks like STOMP headers, or nothing
58
- hash = {}
59
- body.to_s.split("\n").each do |line|
60
- k,v = line.split(':', 2).each{|x| x.strip! }
61
- hash[k] = v
62
- end
63
-
64
- end
65
-
66
- hash
67
- end
68
-
69
-
70
25
  ##
71
26
  # :call-seq:
72
27
  # StompHandler.with_timeout(secs) -> (nil)
@@ -79,17 +34,16 @@ module NebulousStomp
79
34
  # r.signal
80
35
  # end
81
36
  #
82
- # Use `r.signal` to signal when the process has finished. You need to
83
- # arrange your own method of working out whether the timeout fired or not.
37
+ # Use `r.signal` to signal when the process has finished. You need to arrange your own method
38
+ # of working out whether the timeout fired or not.
84
39
  #
85
- # Also, please note that when the timeout period expires, your code will
86
- # keep running. The timeout will only be honoured when your block
87
- # completes. This is very useful for Stomp.subscribe, but probably not
88
- # for anything else...
40
+ # Also, please note that when the timeout period expires, your code will keep running. The
41
+ # timeout will only be honoured when your block completes. This is very useful for
42
+ # Stomp.subscribe, but probably not for anything else...
89
43
  #
90
- # There is a Ruby standard library for this, Timeout. But there appears to
91
- # be some argument as to whether it is threadsafe; so, we roll our own. It
92
- # probably doesn't matter since both Redis and Stomp do use Timeout. But.
44
+ # There is a Ruby standard library for this, Timeout. But there appears to be some argument
45
+ # as to whether it is threadsafe; so, we roll our own. It probably doesn't matter since both
46
+ # Redis and Stomp do use Timeout. But.
93
47
  #
94
48
  def with_timeout(secs)
95
49
  mutex = Mutex.new
@@ -98,7 +52,6 @@ module NebulousStomp
98
52
  t = Thread.new do
99
53
  mutex.synchronize { yield resource }
100
54
  end
101
-
102
55
  mutex.synchronize { resource.wait(mutex, secs) }
103
56
 
104
57
  nil
@@ -111,18 +64,12 @@ module NebulousStomp
111
64
  ##
112
65
  # Initialise StompHandler by passing the parameter hash.
113
66
  #
114
- # If no hash is set we try and get it from NebulousStomp::Param.
115
- # ONLY set testClient when testing.
116
- #
117
67
  def initialize(connectHash=nil, testClient=nil)
118
- @stomp_hash = connectHash ? connectHash.dup : nil
119
- @stomp_hash ||= Param.get(:stompConnectHash)
120
-
68
+ @stomp_hash = connectHash ? connectHash.dup : nil
121
69
  @test_client = testClient
122
70
  @client = nil
123
71
  end
124
72
 
125
-
126
73
  ##
127
74
  # Connect to the STOMP client.
128
75
  #
@@ -131,20 +78,18 @@ module NebulousStomp
131
78
  NebulousStomp.logger.info(__FILE__) {"Connecting to STOMP"}
132
79
 
133
80
  @client = @test_client || Stomp::Client.new( @stomp_hash )
134
- raise ConnectionError, "Stomp Connection failed" unless connected?
81
+ fail ConnectionError, "Stomp Connection failed" unless connected?
135
82
 
136
83
  conn = @client.connection_frame()
137
84
  if conn.command == Stomp::CMD_ERROR
138
- raise ConnectionError, "Connect Error: #{conn.body}"
85
+ fail ConnectionError, "Connect Error: #{conn.body}"
139
86
  end
140
87
 
141
88
  self
142
-
143
89
  rescue => err
144
90
  raise ConnectionError, err
145
91
  end
146
92
 
147
-
148
93
  ##
149
94
  # Drop the connection to the STOMP Client
150
95
  #
@@ -158,7 +103,6 @@ module NebulousStomp
158
103
  self
159
104
  end
160
105
 
161
-
162
106
  ##
163
107
  # return true if we are connected to the STOMP server
164
108
  #
@@ -166,7 +110,6 @@ module NebulousStomp
166
110
  @client && @client.open?
167
111
  end
168
112
 
169
-
170
113
  ##
171
114
  # return true if Nebulous is turned on in the parameters
172
115
  #
@@ -174,17 +117,19 @@ module NebulousStomp
174
117
  @stomp_hash && !@stomp_hash.empty?
175
118
  end
176
119
 
177
-
178
120
  ##
179
121
  # Block for incoming messages on a queue. Yield each message.
180
122
  #
181
- # Note that the blocking happens in a thread somewhere inside the STOMP
182
- # client. I have no idea how to join that, and if the examples on the STOMP
183
- # gem are to be believed, you flat out can't -- the examples just have the
184
- # main thread sleeping so that it does not termimate while the thread is
185
- # running. So to use this make sure that you at some point do something
123
+ # This method automatically consumes every message it reads, since the assumption is that we
124
+ # are using it for the request-response use case. If you don't want that, try
125
+ # listen_with_timeout(), instead.
126
+ #
127
+ # Note that the blocking happens in a thread somewhere inside the STOMP client. I have no idea
128
+ # how to join that, and if the examples on the STOMP gem are to be believed, you flat out can't
129
+ # -- the examples just have the main thread sleeping so that it does not termimate while the
130
+ # thread is running. So to use this make sure that you at some point do something
186
131
  # like:
187
- # loop; sleep 5; end
132
+ # loop { sleep 5 }
188
133
  #
189
134
  def listen(queue)
190
135
  return unless nebulous_on?
@@ -207,10 +152,9 @@ module NebulousStomp
207
152
 
208
153
  end
209
154
 
210
-
211
155
  ##
212
- # As listen() but give up after yielding a single message, and only wait
213
- # for a set number of seconds before giving up anyway.
156
+ # As listen() but give up after yielding a single message, and only wait for a set number of
157
+ # seconds before giving up anyway.
214
158
  #
215
159
  # The behaviour here is slightly different than listen(). If you return true from your block,
216
160
  # the message will be consumed and the method will end. Otherwise it will continue until it
@@ -221,15 +165,10 @@ module NebulousStomp
221
165
  #
222
166
  def listen_with_timeout(queue, timeout)
223
167
  return unless nebulous_on?
224
-
225
- NebulousStomp.logger.info(__FILE__) do
226
- "Subscribing to #{queue} with timeout #{timeout}"
227
- end
168
+ NebulousStomp.logger.info(__FILE__) { "Subscribing to #{queue} with timeout #{timeout}" }
228
169
 
229
170
  stomp_connect unless @client
230
-
231
171
  @client.publish( queue, "boo" )
232
-
233
172
  done = false
234
173
 
235
174
  StompHandler.with_timeout(timeout) do |resource|
@@ -258,16 +197,15 @@ module NebulousStomp
258
197
  resource.signal if done #or here. either, but.
259
198
  end # of with_timeout
260
199
 
261
- raise NebulousTimeout unless done
200
+ fail NebulousTimeout unless done
262
201
  end
263
202
 
264
-
265
203
  ##
266
204
  # Send a Message to a queue; return the message.
267
205
  #
268
206
  def send_message(queue, mess)
269
207
  return nil unless nebulous_on?
270
- raise NebulousStomp::NebulousError, "That's not a Message" \
208
+ fail NebulousStomp::NebulousError, "That's not a Message" \
271
209
  unless mess.respond_to?(:body_for_stomp) \
272
210
  && mess.respond_to?(:headers_for_stomp)
273
211
 
@@ -275,17 +213,15 @@ module NebulousStomp
275
213
 
276
214
  headers = mess.headers_for_stomp.reject{|k,v| v.nil? || v == "" }
277
215
  @client.publish(queue, mess.body_for_stomp, headers)
278
-
279
216
  mess
280
217
  end
281
218
 
282
-
283
219
  ##
284
220
  # Return the neb-reply-id we're going to use for this connection
285
221
  #
286
222
  def calc_reply_id
287
223
  return nil unless nebulous_on?
288
- raise ConnectionError, "Client not connected" unless @client
224
+ fail ConnectionError, "Client not connected" unless @client
289
225
 
290
226
  @client.connection_frame().headers["session"] \
291
227
  << "_" \
@@ -293,9 +229,7 @@ module NebulousStomp
293
229
 
294
230
  end
295
231
 
296
-
297
- end
298
- ##
232
+ end # StompHandler
299
233
 
300
234
 
301
235
  end
@@ -10,85 +10,71 @@ module NebulousStomp
10
10
 
11
11
 
12
12
  ##
13
- # Behaves just like StompHandler, except, does nothing and expects no stomp
14
- # connection
13
+ # Behaves just like StompHandler, except, does nothing and expects no stomp connection
15
14
  #
16
15
  class StompHandlerNull < StompHandler
17
16
 
18
- attr_reader :fake_mess
19
-
17
+ attr_reader :fake_messages
20
18
 
21
19
  def initialize(hash={})
22
20
  super(hash)
23
- @fake_mess = nil
21
+ @fake_messages = []
24
22
  end
25
23
 
26
-
27
24
  def insert_fake(message)
28
- @fake_mess = message
25
+ @fake_messages << message
29
26
  end
30
27
 
31
-
32
28
  def stomp_connect
33
29
  NebulousStomp.logger.info(__FILE__) {"Connecting to STOMP (Null)"}
34
-
35
30
  @client = true
36
31
  self
37
32
  end
38
33
 
39
-
40
34
  def stomp_disconnect
41
35
  NebulousStomp.logger.info(__FILE__) {"STOMP Disconnect (Null)"}
42
36
  @client = nil
43
37
  self
44
38
  end
45
-
46
39
 
47
40
  def connected?
48
- @fake_mess != nil
41
+ @fake_messages != []
49
42
  end
50
43
 
51
-
52
44
  def listen(queue)
53
45
  NebulousStomp.logger.info(__FILE__) {"Subscribing to #{queue} (on Null)"}
54
- yield @fake_mess
46
+ @fake_messages.each{|m| yield m }
55
47
  end
56
48
 
57
-
58
49
  def listen_with_timeout(queue, timeout)
59
50
  NebulousStomp.logger.info(__FILE__) {"Subscribing to #{queue} (on Null)"}
60
51
 
61
- if @fake_mess
62
- yield @fake_mess
52
+ if @fake_messages != []
53
+ @fake_messages.each{|m| yield m }
63
54
  else
64
55
  sleep timeout
65
56
  raise NebulousStomp::NebulousTimeout
66
57
  end
67
58
  end
68
59
 
69
-
70
60
  def send_message(queue, nebMess)
71
61
  nebMess
72
62
  end
73
63
 
74
-
75
64
  def respond_success(nebMess)
76
65
  NebulousStomp.logger.info(__FILE__) do
77
66
  "Responded to #{nebMess} with 'success' verb (to Null)"
78
67
  end
79
68
  end
80
69
 
81
-
82
70
  def respond_error(nebMess,err,fields=[])
83
71
  NebulousStomp.logger.info(__FILE__) do
84
72
  "Responded to #{nebMess} with 'error' verb: #{err} (to Null)"
85
73
  end
86
74
  end
87
75
 
88
-
89
76
  def calc_reply_id; 'ABCD123456789'; end
90
77
 
91
-
92
78
  end
93
79
 
94
80
 
@@ -0,0 +1,52 @@
1
+ module NebulousStomp
2
+
3
+
4
+ ##
5
+ # Represents a single Target. Read only.
6
+ #
7
+ # NebulousStomp.add_target returns a Target, or you can retreive one from the config using
8
+ # NebulousStomp.get_target.
9
+ #
10
+ class Target
11
+ #
12
+ # The identifying name of the queue
13
+ attr_reader :name
14
+
15
+ # The queue that the target sends responses to
16
+ attr_reader :send_queue
17
+
18
+ # The queue that the target listens for requests on
19
+ attr_reader :receive_queue
20
+
21
+ # The message timeout for the queue
22
+ attr_reader :message_timeout
23
+
24
+ VALID_KEYS = %i|sendQueue receiveQueue messageTimeout name|
25
+
26
+ ##
27
+ # Create a target.
28
+ #
29
+ # Valid keys for the hash:
30
+ #
31
+ # * :sendQueue
32
+ # * :receiveQeue
33
+ # * :name
34
+ # * :messageTimeout (optional)
35
+ #
36
+ def initialize(hash)
37
+ fail ArgumentError, "Argument for Target.new must be a hash" unless hash.is_a? Hash
38
+
39
+ @send_queue = hash[:sendQueue] or fail ArgumentError, "Missing a sendQueue"
40
+ @receive_queue = hash[:receiveQueue] or fail ArgumentError, "Missing a receiveQueue"
41
+ @name = hash[:name] or fail ArgumentError, "Missing a name"
42
+ @message_timeout = hash[:messageTimeout]
43
+
44
+ bad_keys = hash.reject{|k, _| VALID_KEYS.include? k }.keys
45
+ fail ArgumentError, "Bad keys: #{bad_keys.join ' '}" unless bad_keys.empty?
46
+ end
47
+
48
+ end
49
+
50
+
51
+ end
52
+
@@ -1,5 +1,5 @@
1
1
  module NebulousStomp
2
2
 
3
3
  # Nebulous version number
4
- VERSION = "2.0.2"
4
+ VERSION = "3.0.0"
5
5
  end