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,290 @@
1
+ # COding: UTF-8
2
+
3
+ require 'stomp'
4
+ require 'json'
5
+ require 'time'
6
+
7
+
8
+ module Nebulous
9
+
10
+
11
+ ##
12
+ # A Class to deal with talking to STOMP via the Stomp gem
13
+ #
14
+ class StompHandler
15
+
16
+ attr_reader :client
17
+
18
+
19
+ ##
20
+ # Class methods
21
+ #
22
+ class << self
23
+
24
+
25
+ ##
26
+ # Parse stomp headers & body and return body as something Ruby-ish.
27
+ # It might not be a hash, in fact -- it could be an array of hashes.
28
+ #
29
+ # We assume that you are getting this from a STOMP message; the routine
30
+ # might not work if it is passed something other than Stomp::Message
31
+ # headers.
32
+ #
33
+ # If you have better intelligence as to the content type of the message,
34
+ # pass the content type as the optional third parameter.
35
+ #
36
+ def body_to_hash(headers, body, contentType=nil)
37
+ hdrs = headers || {}
38
+
39
+ raise ArgumentError, "headers is not a hash" \
40
+ unless hdrs.kind_of? Hash
41
+
42
+ type = contentType \
43
+ || hdrs["content-type"] || hdrs[:content_type] \
44
+ || hdrs["contentType"] || hdrs[:contentType]
45
+
46
+ hash = nil
47
+
48
+ if type =~ /json$/i
49
+ begin
50
+ hash = JSON.parse(body)
51
+ rescue JSON::ParserError, TypeError
52
+ hash = {}
53
+ end
54
+
55
+ else
56
+ # We assume that text looks like STOMP headers, or nothing
57
+ hash = {}
58
+ body.to_s.split("\n").each do |line|
59
+ k,v = line.split(':', 2).each{|x| x.strip! }
60
+ hash[k] = v
61
+ end
62
+
63
+ end
64
+
65
+ hash
66
+ end
67
+
68
+
69
+ ##
70
+ # :call-seq:
71
+ # StompHandler.with_timeout(secs) -> (nil)
72
+ #
73
+ # Run a routine with a timeout.
74
+ #
75
+ # Example:
76
+ # StompHandler.with_timeout(10) do |r|
77
+ # sleep 20
78
+ # r.signal
79
+ # end
80
+ #
81
+ # Use `r.signal` to signal when the process has finished. You need to
82
+ # arrange your own method of working out whether the timeout fired or not.
83
+ #
84
+ # Also, please note that when the timeout period expires, your code will
85
+ # keep running. The timeout will only be honoured when your block
86
+ # completes. This is very useful for Stomp.subscribe, but probably not
87
+ # for anything else...
88
+ #
89
+ # There is a Ruby standard library for this, Timeout. But there appears to
90
+ # be some argument as to whether it is threadsafe; so, we roll our own. It
91
+ # probably doesn't matter since both Redis and Stomp do use Timeout. But.
92
+ #
93
+ def with_timeout(secs)
94
+ mutex = Mutex.new
95
+ resource = ConditionVariable.new
96
+
97
+ t = Thread.new do
98
+ mutex.synchronize { yield resource }
99
+ end
100
+
101
+ mutex.synchronize { resource.wait(mutex, secs) }
102
+
103
+ nil
104
+ end
105
+
106
+ end
107
+ ##
108
+
109
+
110
+ ##
111
+ # Initialise StompHandler by passing the parameter hash.
112
+ # ONLY set testClient when testing.
113
+ #
114
+ def initialize(connectHash, testClient=nil)
115
+ @stomp_hash = connectHash ? connectHash.dup : nil
116
+ @test_client = testClient
117
+ @client = nil
118
+ end
119
+
120
+
121
+ ##
122
+ # Connect to the STOMP client.
123
+ #
124
+ def stomp_connect
125
+ return self unless nebulous_on?
126
+ Nebulous.logger.info(__FILE__) {"Connecting to STOMP"}
127
+
128
+ @client = @test_client || Stomp::Client.new( @stomp_hash.dup )
129
+ raise ConnectionError, "Stomp Connection failed" unless connected?
130
+
131
+ conn = @client.connection_frame()
132
+ if conn.command == Stomp::CMD_ERROR
133
+ raise ConnectionError, "Connect Error: #{conn.body}"
134
+ end
135
+
136
+ self
137
+
138
+ rescue => err
139
+ raise ConnectionError, err
140
+ end
141
+
142
+
143
+ ##
144
+ # Drop the connection to the STOMP Client
145
+ #
146
+ def stomp_disconnect
147
+ if @client
148
+ Nebulous.logger.info(__FILE__) {"STOMP Disconnect"}
149
+ @client.close if @client
150
+ @client = nil
151
+ end
152
+
153
+ self
154
+ end
155
+
156
+
157
+ ##
158
+ # return true if we are connected to the STOMP server
159
+ #
160
+ def connected?
161
+ @client && @client.open?
162
+ end
163
+
164
+
165
+ ##
166
+ # return true if Nebulous is turned on in the parameters
167
+ #
168
+ def nebulous_on?
169
+ @stomp_hash && !@stomp_hash.empty?
170
+ end
171
+
172
+
173
+ ##
174
+ # Block for incoming messages on a queue. Yield each message.
175
+ #
176
+ # Note that the blocking happens in a thread somewhere inside the STOMP
177
+ # client. I have no idea how to join that, and if the examples on the STOMP
178
+ # gem are to be believed, you flat out can't -- the examples just have the
179
+ # main thread sleeping so that it does not termimate while the thread is
180
+ # running. So to use this make sure that you at some point do something
181
+ # like:
182
+ # loop; sleep 5; end
183
+ #
184
+ def listen(queue)
185
+ return unless nebulous_on?
186
+ Nebulous.logger.info(__FILE__) {"Subscribing to #{queue}"}
187
+
188
+ stomp_connect unless @client
189
+
190
+ # Startle the queue into existence. You can't subscribe to a queue that
191
+ # does not exist, BUT, you can create a queue by posting to it...
192
+ @client.publish( queue, "boo" )
193
+
194
+ @client.subscribe( queue, {ack: "client-individual"} ) do |msg|
195
+ begin
196
+ @client.ack(msg)
197
+ yield Message.from_stomp(msg) unless msg.body == 'boo'
198
+ rescue =>e
199
+ Nebulous.logger.error(__FILE__) {"Error during polling: #{e}" }
200
+ end
201
+ end
202
+
203
+ end
204
+
205
+
206
+ ##
207
+ # As listen() but give up after yielding a single message, and only wait
208
+ # for a set number of seconds before giving up anyway.
209
+ #--
210
+ # Ideally I'd like to DRY this and listen() up, but with this
211
+ # yield-within-a-thread stuff going on, I'm actually not sure how to do
212
+ # that safely.
213
+ #
214
+ # Actually i'm not even sure how to stop once I've read one message. The
215
+ # Stomp gem behaves very strangely.
216
+ #++
217
+ #
218
+ def listen_with_timeout(queue, timeout)
219
+ return unless nebulous_on?
220
+
221
+ Nebulous.logger.info(__FILE__) do
222
+ "Subscribing to #{queue} with timeout #{timeout}"
223
+ end
224
+
225
+ stomp_connect unless @client
226
+
227
+ @client.publish( queue, "boo" )
228
+
229
+ done = false
230
+
231
+ StompHandler.with_timeout(timeout) do |resource|
232
+ @client.subscribe( queue, {ack: "client-individual"} ) do |msg|
233
+
234
+ begin
235
+ if msg.body == 'boo'
236
+ @client.ack(msg)
237
+ elsif done == false
238
+ yield Message.from_stomp(msg)
239
+ done = true
240
+ end
241
+ rescue =>e
242
+ Nebulous.logger.error(__FILE__) {"Error during polling: #{e}" }
243
+ end
244
+
245
+ end # of Stomp client subscribe block
246
+
247
+ # Not that this seems to do any good when the Stomp gem is in play, but.
248
+ resource.signal if done
249
+
250
+ end # of with_timeout
251
+
252
+ raise NebulousTimeout unless done
253
+ end
254
+
255
+
256
+ ##
257
+ # Send a Message to a queue; return the message.
258
+ #
259
+ def send_message(queue, mess)
260
+ return nil unless nebulous_on?
261
+ raise Nebulous::NebulousError, "That's not a Message" \
262
+ unless mess.respond_to?(:body_for_stomp) \
263
+ && mess.respond_to?(:headers_for_stomp)
264
+
265
+ stomp_connect unless @client
266
+ @client.publish(queue, mess.body_for_stomp, mess.headers_for_stomp)
267
+ mess
268
+ end
269
+
270
+
271
+ ##
272
+ # Return the neb-reply-id we're going to use for this connection
273
+ #
274
+ def calc_reply_id
275
+ return nil unless nebulous_on?
276
+ raise ConnectionError, "Client not connected" unless @client
277
+
278
+ @client.connection_frame().headers["session"] \
279
+ << "_" \
280
+ << Time.now.to_f.to_s
281
+
282
+ end
283
+
284
+
285
+ end
286
+ ##
287
+
288
+
289
+ end
290
+
@@ -0,0 +1,98 @@
1
+ # COding: UTF-8
2
+
3
+ require 'stomp'
4
+ require 'json'
5
+ require 'time'
6
+
7
+ require_relative 'stomp_handler'
8
+ require_relative 'message'
9
+
10
+
11
+ module Nebulous
12
+
13
+
14
+ ##
15
+ # Behaves just like StompHandler, except, does nothing and expects no stomp
16
+ # connection
17
+ #
18
+ class StompHandlerNull < StompHandler
19
+
20
+ attr_reader :fake_mess
21
+
22
+
23
+ def initialize(hash={})
24
+ super(hash)
25
+ @fake_mess = nil
26
+ end
27
+
28
+
29
+ def insert_fake(message)
30
+ @fake_mess = message
31
+ end
32
+
33
+
34
+ def stomp_connect
35
+ Nebulous.logger.info(__FILE__) {"Connecting to STOMP (Null)"}
36
+
37
+ @client = true
38
+ self
39
+ end
40
+
41
+
42
+ def stomp_disconnect
43
+ Nebulous.logger.info(__FILE__) {"STOMP Disconnect (Null)"}
44
+ @client = nil
45
+ self
46
+ end
47
+
48
+
49
+ def connected?
50
+ @fake_mess != nil
51
+ end
52
+
53
+
54
+ def listen(queue)
55
+ Nebulous.logger.info(__FILE__) {"Subscribing to #{queue} (on Null)"}
56
+ yield @fake_mess
57
+ end
58
+
59
+
60
+ def listen_with_timeout(queue, timeout)
61
+ Nebulous.logger.info(__FILE__) {"Subscribing to #{queue} (on Null)"}
62
+
63
+ if @fake_mess
64
+ yield @fake_mess
65
+ else
66
+ sleep timeout
67
+ raise Nebulous::NebulousTimeout
68
+ end
69
+ end
70
+
71
+
72
+ def send_message(queue, nebMess)
73
+ nebMess
74
+ end
75
+
76
+
77
+ def respond_success(nebMess)
78
+ Nebulous.logger.info(__FILE__) do
79
+ "Responded to #{nebMess} with 'success' verb (to Null)"
80
+ end
81
+ end
82
+
83
+
84
+ def respond_error(nebMess,err,fields=[])
85
+ Nebulous.logger.info(__FILE__) do
86
+ "Responded to #{nebMess} with 'error' verb: #{err} (to Null)"
87
+ end
88
+ end
89
+
90
+
91
+ def calc_reply_id; 'ABCD123456789'; end
92
+
93
+
94
+ end
95
+
96
+
97
+ end
98
+
@@ -0,0 +1,5 @@
1
+ module Nebulous
2
+
3
+ # Nebulous version number
4
+ VERSION = "1.1.5"
5
+ end
data/lib/nebulous.rb ADDED
@@ -0,0 +1,140 @@
1
+ # coding: UTF-8
2
+
3
+ require 'stomp'
4
+ require 'redis'
5
+ require 'logger'
6
+ require 'devnull'
7
+
8
+ require 'nebulous/version'
9
+ require 'nebulous/param'
10
+ require 'nebulous/message'
11
+ require 'nebulous/nebrequest'
12
+ require 'nebulous/stomp_handler'
13
+ require 'nebulous/redis_handler'
14
+
15
+
16
+ ##
17
+ # A little module that implements the Nebulous Protocol, a way of passing data
18
+ # over STOMP between different systems. We also support message cacheing via
19
+ # Redis.
20
+ #
21
+ # There are two use cases:
22
+ #
23
+ # First, sending a request for information and waiting for a response, which
24
+ # might come from a cache of previous responses, if you allow it. To do
25
+ # this you should create a Nebulous::NebRequest, which will return a
26
+ # Nebulous::Message.
27
+ #
28
+ # Second, the other end of the deal: hanging around waiting for requests and
29
+ # sending responses. To do this, you need to use the Nebulous::StompHandler
30
+ # class, which will again furnish Nebulous::Meessage objects, and allow you to
31
+ # create them.
32
+ #
33
+ # Some configuratuion is required: see Nebulous.init, Nebulous.add_target &
34
+ # Nebulous.add_logger.
35
+ #
36
+ # Since you are setting the Redis connection details as part of initialisation,
37
+ # you can also use it to connect to Redis, if you want. See
38
+ # Nebulous::RedisHandler.
39
+ #
40
+ # a complete list of classes & modules:
41
+ #
42
+ # * Nebulous
43
+ # * Nebulous::Param
44
+ # * Nebulous::NebRequest
45
+ # * Nebulous::NebRequestNull
46
+ # * Nebulous::Message
47
+ # * Nebulous::StompHandler
48
+ # * Nebulous::StompHandlerNull
49
+ # * Nebulous::RedisHandler
50
+ # * Nebulous::RedisHandlerNull
51
+ #
52
+ # If you want the null classes, you must require them seperately.
53
+ module Nebulous
54
+
55
+
56
+ # Thrown when anything goes wrong.
57
+ class NebulousError < StandardError; end
58
+
59
+ # Thrown when nothing went wrong, but a timeout expired.
60
+ class NebulousTimeout < StandardError; end
61
+
62
+ # Thrown when we can't connect to STOMP or the connection is lost somehow
63
+ class ConnectionError < NebulousError; end
64
+
65
+
66
+ # :call-seq:
67
+ # Nebulous.init(paramHash) -> (nil)
68
+ #
69
+ # Initialise library for use and override default options with any in
70
+ # <paramHash>.
71
+ #
72
+ # The default options are defined in Nebulous::Param.
73
+ #
74
+ def self.init(paramHash={})
75
+ Param.set(paramHash)
76
+ return nil
77
+ end
78
+
79
+
80
+ # :call-seq:
81
+ # Nebulous.add_target(name, targetHash) -> (nil)
82
+ #
83
+ # Add a nebulous target called <name> with a details as per <targetHash>.
84
+ #
85
+ # <targetHash> must contain a send queue and a receive queue, or a
86
+ # NebulousError will be thrown. Have a look in Nebulous::Param for the
87
+ # default hash you are overriding here.
88
+ #
89
+ def self.add_target(name, targetHash) # -> nil
90
+ Param.add_target(name, targetHash)
91
+ return nil
92
+ end
93
+
94
+
95
+ ##
96
+ # Set an instance of Logger to log stuff to.
97
+ def self.set_logger(logger)
98
+ Param.set_logger(logger)
99
+ end
100
+
101
+
102
+ ##
103
+ # :call-seq:
104
+ # Nebulous.logger.info(__FILE__) { "message" }
105
+ #
106
+ # Return a Logger instance to log things to.
107
+ # If one was not given to Param, return a logger instance that
108
+ # uses a DevNull IO object, that is, goes nowhere.
109
+ #
110
+ def self.logger
111
+ Param.get_logger || Logger.new( DevNull.new )
112
+ end
113
+
114
+
115
+ ##
116
+ # :call-seq:
117
+ # Nebulous.on? -> Boolean
118
+ #
119
+ # True if Nebulous is configured to be running
120
+ #
121
+ def self.on?
122
+ h = Param.get(:stompConnectHash)
123
+ !(h.nil? || h.empty?)
124
+ end
125
+
126
+
127
+ ##
128
+ # :call-seq:
129
+ # Nebulous.redis_on? -> Boolean
130
+ #
131
+ # True if the Redis cache is configured to be running
132
+ #
133
+ def self.redis_on?
134
+ h = Param.get(:redisConnectHash)
135
+ !(h.nil? || h.empty?)
136
+ end
137
+
138
+
139
+ end
140
+
data/md/LICENSE.txt ADDED
@@ -0,0 +1,3 @@
1
+ Copyright (c) 2015 James Hall & Co Ltd
2
+
3
+ Permission to use copy etc etc withheld. No-one is reading this, but.