stomp 1.0.6 → 1.1

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,207 @@
1
+ module Stomp
2
+
3
+ # Typical Stomp client class. Uses a listener thread to receive frames
4
+ # from the server, any thread can send.
5
+ #
6
+ # Receives all happen in one thread, so consider not doing much processing
7
+ # in that thread if you have much message volume.
8
+ class Client
9
+
10
+ attr_reader :login, :passcode, :host, :port, :reliable, :running
11
+
12
+ # A new Client object can be initialized using two forms:
13
+ #
14
+ # Standard positional parameters:
15
+ # login (String, default : '')
16
+ # passcode (String, default : '')
17
+ # host (String, default : 'localhost')
18
+ # port (Integer, default : 61613)
19
+ # reliable (Boolean, default : false)
20
+ #
21
+ # e.g. c = Client.new('login', 'passcode', 'localhost', 61613, true)
22
+ #
23
+ # Stomp URL :
24
+ # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
25
+ #
26
+ # stomp://host:port
27
+ # stomp://host.domain.tld:port
28
+ # stomp://login:passcode@host:port
29
+ # stomp://login:passcode@host.domain.tld:port
30
+ #
31
+ def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
32
+
33
+ # Parse stomp:// URL's or set positional params
34
+ case login
35
+ when /stomp:\/\/([\w\.]+):(\d+)/ # e.g. stomp://host:port
36
+ # grabs the matching positions out of the regex which are stored as
37
+ # $1 (host), $2 (port), etc
38
+ @login = ''
39
+ @passcode = ''
40
+ @host = $1
41
+ @port = $2.to_i
42
+ @reliable = false
43
+ when /stomp:\/\/([\w\.]+):(\w+)@([\w\.]+):(\d+)/ # e.g. stomp://login:passcode@host:port
44
+ @login = $1
45
+ @passcode = $2
46
+ @host = $3
47
+ @port = $4.to_i
48
+ @reliable = false
49
+ else
50
+ @login = login
51
+ @passcode = passcode
52
+ @host = host
53
+ @port = port.to_i
54
+ @reliable = reliable
55
+ end
56
+
57
+ raise ArgumentError if @host.nil? || @host.empty?
58
+ raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
59
+ raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
60
+
61
+ @id_mutex = Mutex.new
62
+ @ids = 1
63
+ @connection = Connection.new(@login, @passcode, @host, @port, @reliable)
64
+ @listeners = {}
65
+ @receipt_listeners = {}
66
+ @running = true
67
+ @replay_messages_by_txn = {}
68
+
69
+ @listener_thread = Thread.start do
70
+ while @running
71
+ message = @connection.receive
72
+ case
73
+ when message.nil?
74
+ break
75
+ when message.command == 'MESSAGE'
76
+ if listener = @listeners[message.headers['destination']]
77
+ listener.call(message)
78
+ end
79
+ when message.command == 'RECEIPT'
80
+ if listener = @receipt_listeners[message.headers['receipt-id']]
81
+ listener.call(message)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ # Syntactic sugar for 'Client.new' See 'initialize' for usage.
90
+ def self.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
91
+ Client.new(login, passcode, host, port, reliable)
92
+ end
93
+
94
+ # Join the listener thread for this client,
95
+ # generally used to wait for a quit signal
96
+ def join
97
+ @listener_thread.join
98
+ end
99
+
100
+ # Begin a transaction by name
101
+ def begin(name, headers = {})
102
+ @connection.begin(name, headers)
103
+ end
104
+
105
+ # Abort a transaction by name
106
+ def abort(name, headers = {})
107
+ @connection.abort(name, headers)
108
+
109
+ # lets replay any ack'd messages in this transaction
110
+ replay_list = @replay_messages_by_txn[name]
111
+ if replay_list
112
+ replay_list.each do |message|
113
+ if listener = @listeners[message.headers['destination']]
114
+ listener.call(message)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ # Commit a transaction by name
121
+ def commit(name, headers = {})
122
+ txn_id = headers[:transaction]
123
+ @replay_messages_by_txn.delete(txn_id)
124
+ @connection.commit(name, headers)
125
+ end
126
+
127
+ # Subscribe to a destination, must be passed a block
128
+ # which will be used as a callback listener
129
+ #
130
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
131
+ def subscribe(destination, headers = {})
132
+ raise "No listener given" unless block_given?
133
+ @listeners[destination] = lambda {|msg| yield msg}
134
+ @connection.subscribe(destination, headers)
135
+ end
136
+
137
+ # Unsubecribe from a channel
138
+ def unsubscribe(name, headers = {})
139
+ @connection.unsubscribe(name, headers)
140
+ @listeners[name] = nil
141
+ end
142
+
143
+ # Acknowledge a message, used when a subscription has specified
144
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
145
+ #
146
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
147
+ def acknowledge(message, headers = {})
148
+ txn_id = headers[:transaction]
149
+ if txn_id
150
+ # lets keep around messages ack'd in this transaction in case we rollback
151
+ replay_list = @replay_messages_by_txn[txn_id]
152
+ if replay_list.nil?
153
+ replay_list = []
154
+ @replay_messages_by_txn[txn_id] = replay_list
155
+ end
156
+ replay_list << message
157
+ end
158
+ if block_given?
159
+ headers['receipt'] = register_receipt_listener lambda {|r| yield r}
160
+ end
161
+ @connection.ack message.headers['message-id'], headers
162
+ end
163
+
164
+ # Send message to destination
165
+ #
166
+ # If a block is given a receipt will be requested and passed to the
167
+ # block on receipt
168
+ #
169
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
170
+ def send(destination, message, headers = {})
171
+ if block_given?
172
+ headers['receipt'] = register_receipt_listener lambda {|r| yield r}
173
+ end
174
+ @connection.send(destination, message, headers)
175
+ end
176
+
177
+ # Is this client open?
178
+ def open?
179
+ @connection.open?
180
+ end
181
+
182
+ # Is this client closed?
183
+ def closed?
184
+ @connection.closed?
185
+ end
186
+
187
+ # Close out resources in use by this client
188
+ def close
189
+ @connection.disconnect
190
+ @running = false
191
+ end
192
+
193
+ private
194
+
195
+ def register_receipt_listener(listener)
196
+ id = -1
197
+ @id_mutex.synchronize do
198
+ id = @ids.to_s
199
+ @ids = @ids.succ
200
+ end
201
+ @receipt_listeners[id] = listener
202
+ id
203
+ end
204
+
205
+ end
206
+ end
207
+
@@ -0,0 +1,256 @@
1
+ module Stomp
2
+
3
+ # Low level connection which maps commands and supports
4
+ # synchronous receives
5
+ class Connection
6
+
7
+
8
+ # A new Connection object accepts the following parameters:
9
+ #
10
+ # login (String, default : '')
11
+ # passcode (String, default : '')
12
+ # host (String, default : 'localhost')
13
+ # port (Integer, default : 61613)
14
+ # reliable (Boolean, default : false)
15
+ # reconnect_delay (Integer, default : 5)
16
+ #
17
+ # e.g. c = Client.new("username", "password", "localhost", 61613, true)
18
+ #
19
+ # TODO
20
+ # Stomp URL :
21
+ # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
22
+ #
23
+ # stomp://host:port
24
+ # stomp://host.domain.tld:port
25
+ # stomp://user:pass@host:port
26
+ # stomp://user:pass@host.domain.tld:port
27
+ #
28
+ def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
29
+ @host = host
30
+ @port = port
31
+ @login = login
32
+ @passcode = passcode
33
+ @transmit_semaphore = Mutex.new
34
+ @read_semaphore = Mutex.new
35
+ @socket_semaphore = Mutex.new
36
+ @reliable = reliable
37
+ @reconnect_delay = reconnect_delay
38
+ @connect_headers = connect_headers
39
+ @closed = false
40
+ @subscriptions = {}
41
+ @failure = nil
42
+ socket
43
+ end
44
+
45
+ # Syntactic sugar for 'Connection.new' See 'initialize' for usage.
46
+ def Connection.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
47
+ Connection.new(login, passcode, host, port, reliable, reconnect_delay, connect_headers)
48
+ end
49
+
50
+ def socket
51
+ # Need to look into why the following synchronize does not work.
52
+ #@read_semaphore.synchronize do
53
+ s = @socket;
54
+ while s.nil? || !@failure.nil?
55
+ @failure = nil
56
+ begin
57
+ s = TCPSocket.open @host, @port
58
+ headers = @connect_headers.clone
59
+ headers[:login] = @login
60
+ headers[:passcode] = @passcode
61
+ _transmit(s, "CONNECT", headers)
62
+ @connect = _receive(s)
63
+ # replay any subscriptions.
64
+ @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
65
+ rescue
66
+ @failure = $!;
67
+ s=nil;
68
+ raise unless @reliable
69
+ $stderr.print "connect failed: " + $! +" will retry in #{@reconnect_delay}\n";
70
+ sleep(@reconnect_delay);
71
+ end
72
+ end
73
+ @socket = s
74
+ return s;
75
+ #end
76
+ end
77
+
78
+ # Is this connection open?
79
+ def open?
80
+ !@closed
81
+ end
82
+
83
+ # Is this connection closed?
84
+ def closed?
85
+ @closed
86
+ end
87
+
88
+ # Begin a transaction, requires a name for the transaction
89
+ def begin(name, headers = {})
90
+ headers[:transaction] = name
91
+ transmit("BEGIN", headers)
92
+ end
93
+
94
+ # Acknowledge a message, used when a subscription has specified
95
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
96
+ #
97
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
98
+ def ack(message_id, headers = {})
99
+ headers['message-id'] = message_id
100
+ transmit("ACK", headers)
101
+ end
102
+
103
+ # Commit a transaction by name
104
+ def commit(name, headers = {})
105
+ headers[:transaction] = name
106
+ transmit("COMMIT", headers)
107
+ end
108
+
109
+ # Abort a transaction by name
110
+ def abort(name, headers = {})
111
+ headers[:transaction] = name
112
+ transmit("ABORT", headers)
113
+ end
114
+
115
+ # Subscribe to a destination, must specify a name
116
+ def subscribe(name, headers = {}, subId = nil)
117
+ headers[:destination] = name
118
+ transmit("SUBSCRIBE", headers)
119
+
120
+ # Store the sub so that we can replay if we reconnect.
121
+ if @reliable
122
+ subId = name if subId.nil?
123
+ @subscriptions[subId] = headers
124
+ end
125
+ end
126
+
127
+ # Unsubscribe from a destination, must specify a name
128
+ def unsubscribe(name, headers = {}, subId = nil)
129
+ headers[:destination] = name
130
+ transmit("UNSUBSCRIBE", headers)
131
+ if @reliable
132
+ subId = name if subId.nil?
133
+ @subscriptions.delete(subId)
134
+ end
135
+ end
136
+
137
+ # Send message to destination
138
+ #
139
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
140
+ def send(destination, message, headers = {})
141
+ headers[:destination] = destination
142
+ transmit("SEND", headers, message)
143
+ end
144
+
145
+ # Close this connection
146
+ def disconnect(headers = {})
147
+ transmit("DISCONNECT", headers)
148
+ @closed = true
149
+ end
150
+
151
+ # Return a pending message if one is available, otherwise
152
+ # return nil
153
+ def poll
154
+ @read_semaphore.synchronize do
155
+ return nil if @socket.nil? || !@socket.ready?
156
+ return receive
157
+ end
158
+ end
159
+
160
+ # Receive a frame, block until the frame is received
161
+ def __old_receive
162
+ # The recive my fail so we may need to retry.
163
+ while TRUE
164
+ begin
165
+ s = socket
166
+ return _receive(s)
167
+ rescue
168
+ @failure = $!;
169
+ raise unless @reliable
170
+ $stderr.print "receive failed: " + $!;
171
+ end
172
+ end
173
+ end
174
+
175
+ def receive
176
+ super_result = __old_receive()
177
+ if super_result.nil? && @reliable
178
+ $stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
179
+ @socket = nil
180
+ super_result = __old_receive()
181
+ end
182
+ return super_result
183
+ end
184
+
185
+ private
186
+
187
+ def _receive( s )
188
+ line = ' '
189
+ @read_semaphore.synchronize do
190
+ line = s.gets while line =~ /^\s*$/
191
+ return nil if line.nil?
192
+
193
+ message = Message.new do |m|
194
+ m.command = line.chomp
195
+ m.headers = {}
196
+ until (line = s.gets.chomp) == ''
197
+ k = (line.strip[0, line.strip.index(':')]).strip
198
+ v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
199
+ m.headers[k] = v
200
+ end
201
+
202
+ if (m.headers['content-length'])
203
+ m.body = s.read m.headers['content-length'].to_i
204
+ c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
205
+ raise "Invalid content length received" unless c == 0
206
+ else
207
+ m.body = ''
208
+ if RUBY_VERSION > '1.9'
209
+ until (c = s.getc.ord) == 0
210
+ m.body << c.chr
211
+ end
212
+ else
213
+ until (c = s.getc) == 0
214
+ m.body << c.chr
215
+ end
216
+ end
217
+ end
218
+ #c = s.getc
219
+ #raise "Invalid frame termination received" unless c == 10
220
+ end # message
221
+ return message
222
+
223
+ end
224
+ end
225
+
226
+ def transmit(command, headers = {}, body = '')
227
+ # The transmit may fail so we may need to retry.
228
+ while TRUE
229
+ begin
230
+ s = socket
231
+ _transmit(s, command, headers, body)
232
+ return
233
+ rescue
234
+ @failure = $!;
235
+ raise unless @reliable
236
+ $stderr.print "transmit failed: " + $!+"\n";
237
+ end
238
+ end
239
+ end
240
+
241
+ def _transmit(s, command, headers = {}, body = '')
242
+ @transmit_semaphore.synchronize do
243
+ s.puts command
244
+ headers.each {|k,v| s.puts "#{k}:#{v}" }
245
+ s.puts "content-length: #{body.length}"
246
+ s.puts "content-type: text/plain; charset=UTF-8"
247
+ s.puts
248
+ s.write body
249
+ s.write "\0"
250
+ end
251
+ end
252
+
253
+ end
254
+
255
+ end
256
+