nebulous_stomp 1.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.hgignore +19 -0
- data/.hgtags +16 -0
- data/.rspec +8 -0
- data/.travis.yml +3 -0
- data/.yardoc/checksums +10 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile +4 -0
- data/Guardfile +17 -0
- data/README.md +38 -0
- data/Rakefile +31 -0
- data/lib/nebulous/message.rb +368 -0
- data/lib/nebulous/nebrequest.rb +254 -0
- data/lib/nebulous/nebrequest_null.rb +39 -0
- data/lib/nebulous/param.rb +139 -0
- data/lib/nebulous/redis_handler.rb +103 -0
- data/lib/nebulous/redis_handler_null.rb +61 -0
- data/lib/nebulous/stomp_handler.rb +290 -0
- data/lib/nebulous/stomp_handler_null.rb +98 -0
- data/lib/nebulous/version.rb +5 -0
- data/lib/nebulous.rb +140 -0
- data/md/LICENSE.txt +3 -0
- data/md/nebulous_protocol.md +193 -0
- data/nebulous.gemspec +46 -0
- data/spec/doc_no_pending.rb +5 -0
- data/spec/helpers.rb +22 -0
- data/spec/message_spec.rb +575 -0
- data/spec/nebrequest_null_spec.rb +223 -0
- data/spec/nebrequest_spec.rb +241 -0
- data/spec/nebulous_spec.rb +124 -0
- data/spec/param_spec.rb +146 -0
- data/spec/redis_handler_null_spec.rb +97 -0
- data/spec/redis_handler_spec.rb +141 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/spec_helper_old.rb +19 -0
- data/spec/stomp_handler_null_spec.rb +173 -0
- data/spec/stomp_handler_spec.rb +446 -0
- data/tags +134 -0
- metadata +245 -0
@@ -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
|
+
|
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