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