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,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.