onstomp 1.0.0pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +2 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +4 -0
- data/DeveloperNarrative.md +15 -0
- data/Gemfile +4 -0
- data/LICENSE.md +221 -0
- data/README.md +73 -0
- data/Rakefile +6 -0
- data/UserNarrative.md +8 -0
- data/examples/basic.rb +40 -0
- data/examples/events.rb +72 -0
- data/lib/onstomp/client.rb +152 -0
- data/lib/onstomp/components/frame.rb +108 -0
- data/lib/onstomp/components/frame_headers.rb +212 -0
- data/lib/onstomp/components/nil_processor.rb +20 -0
- data/lib/onstomp/components/scopes/header_scope.rb +25 -0
- data/lib/onstomp/components/scopes/receipt_scope.rb +25 -0
- data/lib/onstomp/components/scopes/transaction_scope.rb +191 -0
- data/lib/onstomp/components/scopes.rb +45 -0
- data/lib/onstomp/components/subscription.rb +30 -0
- data/lib/onstomp/components/threaded_processor.rb +62 -0
- data/lib/onstomp/components/uri.rb +30 -0
- data/lib/onstomp/components.rb +13 -0
- data/lib/onstomp/connections/base.rb +208 -0
- data/lib/onstomp/connections/heartbeating.rb +82 -0
- data/lib/onstomp/connections/serializers/stomp_1.rb +166 -0
- data/lib/onstomp/connections/serializers/stomp_1_0.rb +41 -0
- data/lib/onstomp/connections/serializers/stomp_1_1.rb +134 -0
- data/lib/onstomp/connections/serializers.rb +9 -0
- data/lib/onstomp/connections/stomp_1.rb +69 -0
- data/lib/onstomp/connections/stomp_1_0.rb +28 -0
- data/lib/onstomp/connections/stomp_1_1.rb +65 -0
- data/lib/onstomp/connections.rb +119 -0
- data/lib/onstomp/interfaces/client_configurable.rb +55 -0
- data/lib/onstomp/interfaces/client_events.rb +168 -0
- data/lib/onstomp/interfaces/connection_events.rb +62 -0
- data/lib/onstomp/interfaces/event_manager.rb +69 -0
- data/lib/onstomp/interfaces/frame_methods.rb +190 -0
- data/lib/onstomp/interfaces/receipt_manager.rb +33 -0
- data/lib/onstomp/interfaces/subscription_manager.rb +48 -0
- data/lib/onstomp/interfaces/uri_configurable.rb +106 -0
- data/lib/onstomp/interfaces.rb +14 -0
- data/lib/onstomp/version.rb +13 -0
- data/lib/onstomp.rb +147 -0
- data/onstomp.gemspec +29 -0
- data/spec/onstomp/client_spec.rb +265 -0
- data/spec/onstomp/components/frame_headers_spec.rb +163 -0
- data/spec/onstomp/components/frame_spec.rb +144 -0
- data/spec/onstomp/components/nil_processor_spec.rb +32 -0
- data/spec/onstomp/components/scopes/header_scope_spec.rb +27 -0
- data/spec/onstomp/components/scopes/receipt_scope_spec.rb +33 -0
- data/spec/onstomp/components/scopes/transaction_scope_spec.rb +227 -0
- data/spec/onstomp/components/scopes_spec.rb +63 -0
- data/spec/onstomp/components/subscription_spec.rb +58 -0
- data/spec/onstomp/components/threaded_processor_spec.rb +92 -0
- data/spec/onstomp/components/uri_spec.rb +33 -0
- data/spec/onstomp/connections/base_spec.rb +349 -0
- data/spec/onstomp/connections/heartbeating_spec.rb +132 -0
- data/spec/onstomp/connections/serializers/stomp_1_0_spec.rb +50 -0
- data/spec/onstomp/connections/serializers/stomp_1_1_spec.rb +99 -0
- data/spec/onstomp/connections/serializers/stomp_1_spec.rb +104 -0
- data/spec/onstomp/connections/stomp_1_0_spec.rb +54 -0
- data/spec/onstomp/connections/stomp_1_1_spec.rb +137 -0
- data/spec/onstomp/connections/stomp_1_spec.rb +113 -0
- data/spec/onstomp/connections_spec.rb +135 -0
- data/spec/onstomp/interfaces/client_events_spec.rb +108 -0
- data/spec/onstomp/interfaces/connection_events_spec.rb +55 -0
- data/spec/onstomp/interfaces/event_manager_spec.rb +72 -0
- data/spec/onstomp/interfaces/frame_methods_spec.rb +109 -0
- data/spec/onstomp/interfaces/receipt_manager_spec.rb +53 -0
- data/spec/onstomp/interfaces/subscription_manager_spec.rb +64 -0
- data/spec/onstomp_spec.rb +15 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/custom_argument_matchers.rb +51 -0
- data/spec/support/frame_matchers.rb +88 -0
- data/spec/support/shared_frame_method_examples.rb +116 -0
- data/yard_extensions.rb +32 -0
- metadata +219 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# An IO processor that does its work on its own thread.
|
4
|
+
class OnStomp::Components::ThreadedProcessor
|
5
|
+
# Creates a new processor for the {OnStomp::Client client}
|
6
|
+
# @param [OnStomp::Client] client
|
7
|
+
def initialize client
|
8
|
+
@client = client
|
9
|
+
@run_thread = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns true if its IO thread has been created and is alive, otherwise
|
13
|
+
# false.
|
14
|
+
# @return [true,false]
|
15
|
+
def running?
|
16
|
+
@run_thread && @run_thread.alive?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Starts the processor by creating a new thread that continually invokes
|
20
|
+
# {OnStomp::Connections::Base#io_process} while the client is
|
21
|
+
# {OnStomp::Client#connected? connected}.
|
22
|
+
# @return [self]
|
23
|
+
def start
|
24
|
+
@run_thread = Thread.new do
|
25
|
+
begin
|
26
|
+
while @client.connected?
|
27
|
+
@client.connection.io_process
|
28
|
+
end
|
29
|
+
rescue OnStomp::StopReceiver
|
30
|
+
rescue Exception
|
31
|
+
raise
|
32
|
+
end
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Causes the thread this method was invoked in to +pass+ until the
|
38
|
+
# processor is no longer {#running? running}.
|
39
|
+
# @return [self]
|
40
|
+
def join
|
41
|
+
Thread.pass while running?
|
42
|
+
@run_thread && @run_thread.join
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Forcefully stops the processor and joins its IO thread to the
|
47
|
+
# callee's thread.
|
48
|
+
# @return [self]
|
49
|
+
# @raise [IOError, SystemCallError] if either were raised in the IO thread
|
50
|
+
# and the {OnStomp::Client client} is still
|
51
|
+
# {OnStomp::Client#connected? connected} after the thread is joined.
|
52
|
+
def stop
|
53
|
+
begin
|
54
|
+
@run_thread.raise OnStomp::StopReceiver if @run_thread.alive?
|
55
|
+
@run_thread.join
|
56
|
+
rescue IOError, SystemCallError
|
57
|
+
raise if @client.connected?
|
58
|
+
end
|
59
|
+
@run_thread = nil
|
60
|
+
self
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Subclasses of URI::Generic to ease working with Stomp URIs.
|
4
|
+
module OnStomp::Components::URI
|
5
|
+
# A URI class for representing URIs with a 'stomp' scheme.
|
6
|
+
class STOMP < ::URI::Generic
|
7
|
+
# The default port to use for these kinds of URI objects when none has
|
8
|
+
# been specified.
|
9
|
+
DEFAULT_PORT = 61613
|
10
|
+
# The type of socket to use with these kinds of URI objects.
|
11
|
+
# @return [:tcp]
|
12
|
+
def onstomp_socket_type; :tcp; end
|
13
|
+
end
|
14
|
+
|
15
|
+
# A URI class for representing URIs with a 'stomp+ssl' scheme.
|
16
|
+
class STOMP_SSL < STOMP
|
17
|
+
# The default port to use for these kinds of URI objects when none has
|
18
|
+
# been specified.
|
19
|
+
DEFAULT_PORT = 61612
|
20
|
+
# The type of socket to use with these kinds of URI objects.
|
21
|
+
# @return [:ssl]
|
22
|
+
def onstomp_socket_type; :ssl; end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add the new URI classes to +URI+'s set of known schemes.
|
27
|
+
module ::URI
|
28
|
+
@@schemes['STOMP'] = OnStomp::Components::URI::STOMP
|
29
|
+
@@schemes['STOMP+SSL'] = OnStomp::Components::URI::STOMP_SSL
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Namespace for various components used by OnStomp library.
|
4
|
+
module OnStomp::Components
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'onstomp/components/uri'
|
8
|
+
require 'onstomp/components/frame_headers'
|
9
|
+
require 'onstomp/components/frame'
|
10
|
+
require 'onstomp/components/subscription'
|
11
|
+
require 'onstomp/components/nil_processor'
|
12
|
+
require 'onstomp/components/threaded_processor'
|
13
|
+
require 'onstomp/components/scopes'
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Common behavior for all connections.
|
4
|
+
class OnStomp::Connections::Base
|
5
|
+
include OnStomp::Interfaces::ConnectionEvents
|
6
|
+
attr_reader :version, :socket, :client
|
7
|
+
attr_reader :last_transmitted_at, :last_received_at
|
8
|
+
|
9
|
+
# The approximate maximum number of bytes to write per call to
|
10
|
+
# {#io_process_write}.
|
11
|
+
MAX_BYTES_PER_WRITE = 1024 * 8
|
12
|
+
# The maximum number of bytes to read per call to {#io_process_read}
|
13
|
+
MAX_BYTES_PER_READ = 1024 * 4
|
14
|
+
|
15
|
+
# Creates a new connection using the given {#socket} object and
|
16
|
+
# {OnStomp::Client client}. The {#socket} object will generally be a +TCPSocket+
|
17
|
+
# or an +OpenSSL::SSL::SSLSocket+ and must support the methods +read_nonblock+
|
18
|
+
# +write_nonblock+, and +close+.
|
19
|
+
# @param [TCPSocket,OpenSSL::SSL::SSLSocket] socket
|
20
|
+
# @param [OnStomp::Client] client
|
21
|
+
def initialize socket, client
|
22
|
+
@socket = socket
|
23
|
+
@write_mutex = Mutex.new
|
24
|
+
@closing = false
|
25
|
+
@write_buffer = []
|
26
|
+
@read_buffer = []
|
27
|
+
@client = client
|
28
|
+
@connection_up = false
|
29
|
+
end
|
30
|
+
|
31
|
+
# Performs any necessary configuration of the connection from the CONNECTED
|
32
|
+
# frame sent by the broker and a +Hash+ of pending callbacks. This method
|
33
|
+
# is called after the protocol negotiation has taken place between client
|
34
|
+
# and broker, and the connection that receives it will be the connection
|
35
|
+
# used by the client for the duration of the session.
|
36
|
+
# @param [OnStomp::Components::Frame] connected
|
37
|
+
# @param [{Symbol => Proc}] con_cbs
|
38
|
+
def configure connected, con_cbs
|
39
|
+
@version = connected.header?(:version) ? connected[:version] : '1.0'
|
40
|
+
install_bindings_from_client con_cbs
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns true if the socket has not been closed, false otherwise.
|
44
|
+
# @return [true,false]
|
45
|
+
def connected?
|
46
|
+
!socket.closed?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Closes the {#socket}. If +blocking+ is true, the socket will be closed
|
50
|
+
# immediately, otherwies the socket will remain open until {#io_process_write}
|
51
|
+
# has finished writing all of its buffered data. Once this method has been
|
52
|
+
# invoked, {#write_frame_nonblock} will not enqueue any additional frames
|
53
|
+
# for writing.
|
54
|
+
# @param [true,false] blocking
|
55
|
+
def close blocking=false
|
56
|
+
@write_mutex.synchronize { @closing = true }
|
57
|
+
if blocking
|
58
|
+
io_process_write until @write_buffer.empty?
|
59
|
+
socket.close
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Exchanges the CONNECT/CONNECTED frame handshake with the broker and returns
|
64
|
+
# the version detected along with the received CONNECTED frame. The supplied
|
65
|
+
# list of headers will be merged into the CONNECT frame sent to the broker.
|
66
|
+
# @param [OnStomp::Client] client
|
67
|
+
# @param [Array<Hash>] headers
|
68
|
+
def connect client, *headers
|
69
|
+
write_frame_nonblock connect_frame(*headers)
|
70
|
+
client_con = nil
|
71
|
+
until client_con
|
72
|
+
io_process_write { |f| client_con ||= f }
|
73
|
+
end
|
74
|
+
broker_con = nil
|
75
|
+
until broker_con
|
76
|
+
io_process_read { |f| broker_con ||= f }
|
77
|
+
end
|
78
|
+
raise OnStomp::ConnectFailedError if broker_con.command != 'CONNECTED'
|
79
|
+
vers = broker_con.header?(:version) ? broker_con[:version] : '1.0'
|
80
|
+
raise OnStomp::UnsupportedProtocolVersionError, vers unless client.versions.include?(vers)
|
81
|
+
@connection_up = true
|
82
|
+
[ vers, broker_con ]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Checks if the missing method ends with '_frame', and if so raises a
|
86
|
+
# {OnStomp::UnsupportedCommandError} exception.
|
87
|
+
# @raise [OnStomp::UnsupportedCommandError]
|
88
|
+
def method_missing meth, *args, &block
|
89
|
+
if meth.to_s =~ /^(.*)_frame$/
|
90
|
+
raise OnStomp::UnsupportedCommandError, $1.upcase
|
91
|
+
else
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Makes a single call to {#io_process_write} and a single call to
|
97
|
+
# {#io_process_read}
|
98
|
+
def io_process &cb
|
99
|
+
io_process_write &cb
|
100
|
+
io_process_read &cb
|
101
|
+
if @connection_up && !connected?
|
102
|
+
triggered_close :died
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Serializes the given frame and adds the data to the connections internal
|
107
|
+
# write buffer
|
108
|
+
# @param [OnStomp::Components::Frame] frame
|
109
|
+
def write_frame_nonblock frame
|
110
|
+
ser = serializer.frame_to_bytes frame
|
111
|
+
push_write_buffer ser, frame
|
112
|
+
end
|
113
|
+
|
114
|
+
# Adds data and frame pair to the end of the write buffer
|
115
|
+
# @param [String] data
|
116
|
+
# @param [OnStomp::Components::Frame]
|
117
|
+
def push_write_buffer data, frame
|
118
|
+
@write_mutex.synchronize {
|
119
|
+
@write_buffer << [data, frame] unless @closing
|
120
|
+
}
|
121
|
+
end
|
122
|
+
# Removes the first data and frame pair from the write buffer
|
123
|
+
# @param [String] data
|
124
|
+
# @param [OnStomp::Components::Frame]
|
125
|
+
def shift_write_buffer
|
126
|
+
@write_mutex.synchronize { @write_buffer.shift }
|
127
|
+
end
|
128
|
+
# Adds the remains of data and frame pair to the head of the write buffer
|
129
|
+
# @param [String] data
|
130
|
+
# @param [OnStomp::Components::Frame]
|
131
|
+
def unshift_write_buffer data, frame
|
132
|
+
@write_mutex.synchronize { @write_buffer.unshift [data, frame] }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Writes serialized frame data to the socket if the write buffer is not
|
136
|
+
# empty and socket is ready for writing. Once a complete frame has
|
137
|
+
# been written, this method will call {OnStomp::Client#dispatch_transmitted}
|
138
|
+
# to notify the client that the frame has been sent to the broker. If a
|
139
|
+
# complete frame cannot be written without blocking, the unsent data is
|
140
|
+
# sent to the head of the write buffer to be processed first the next time
|
141
|
+
# this method is invoked.
|
142
|
+
def io_process_write
|
143
|
+
if @write_buffer.length > 0 && IO.select(nil, [socket], nil, 0.1)
|
144
|
+
to_shift = @write_buffer.length / 3
|
145
|
+
written = 0
|
146
|
+
while written < MAX_BYTES_PER_WRITE
|
147
|
+
data, frame = shift_write_buffer
|
148
|
+
break unless data && connected?
|
149
|
+
begin
|
150
|
+
w = socket.write_nonblock(data)
|
151
|
+
written += w
|
152
|
+
@last_transmitted_at = Time.now
|
153
|
+
if w < data.length
|
154
|
+
unshift_write_buffer data[w..-1], frame
|
155
|
+
else
|
156
|
+
yield frame if block_given?
|
157
|
+
client.dispatch_transmitted frame
|
158
|
+
end
|
159
|
+
rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
|
160
|
+
# writing will either block, or cannot otherwise be completed,
|
161
|
+
# put data back and try again some other day
|
162
|
+
unshift_write_buffer data, frame
|
163
|
+
break
|
164
|
+
rescue Exception
|
165
|
+
triggered_close :terminated
|
166
|
+
raise
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
if @write_buffer.empty? && @closing
|
171
|
+
triggered_close
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Reads serialized frame data from the socket if we're connected and
|
176
|
+
# and the socket is ready for reading. The received data will be pushed
|
177
|
+
# to the end of a read buffer, which is then sent to the connection's
|
178
|
+
# {OnStomp::Connections::Serializers serializer} for processing.
|
179
|
+
def io_process_read
|
180
|
+
if connected? && IO.select([socket], nil, nil, 0.1)
|
181
|
+
begin
|
182
|
+
data = socket.read_nonblock(MAX_BYTES_PER_READ)
|
183
|
+
@read_buffer << data
|
184
|
+
@last_received_at = Time.now
|
185
|
+
serializer.bytes_to_frame(@read_buffer) do |frame|
|
186
|
+
yield frame if block_given?
|
187
|
+
client.dispatch_received frame
|
188
|
+
end
|
189
|
+
rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
|
190
|
+
# do not
|
191
|
+
rescue EOFError
|
192
|
+
triggered_close
|
193
|
+
rescue Exception
|
194
|
+
triggered_close :terminated
|
195
|
+
raise
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
def triggered_close *evs
|
202
|
+
@connection_up = false
|
203
|
+
@closing = false
|
204
|
+
socket.close
|
205
|
+
evs.each { |ev| trigger_connection_event ev }
|
206
|
+
trigger_connection_event :closed
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Mixin for connections to include heartbeating functionality.
|
4
|
+
module OnStomp::Connections::Heartbeating
|
5
|
+
# A pair of integers indicating the maximum number of milliseconds the
|
6
|
+
# client and broker can go without transmitting data respectively. If
|
7
|
+
# either value is 0, the respective heartbeating is not enabled.
|
8
|
+
# @return [[Fixnum, Fixnum]]
|
9
|
+
attr_reader :heartbeating
|
10
|
+
|
11
|
+
# Configures heartbeating strategy by taking the maximum timeout
|
12
|
+
# for clients and brokers. If either pair contains a zero, the respective
|
13
|
+
# beating is disabled.
|
14
|
+
# @param [[Fixnum, Fixnum]] client_beats
|
15
|
+
# @param [[Fixnum, Fixnum]] broker_beats
|
16
|
+
def configure_heartbeating client_beats, broker_beats
|
17
|
+
c_x, c_y = client_beats
|
18
|
+
s_x, s_y = broker_beats
|
19
|
+
@heartbeating = [ (c_x == 0||s_y == 0 ? 0 : [c_x,s_y].max),
|
20
|
+
(c_y == 0||s_x == 0 ? 0 : [c_y,s_x].max) ]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns true if both the client and broker are transmitting data in
|
24
|
+
# accordance with the heartbeating strategy. If this method returns false,
|
25
|
+
# the connection is effectively dead and should be {OnStomp::Connections::Base#close closed}
|
26
|
+
# @return [true, false]
|
27
|
+
def pulse?
|
28
|
+
client_pulse? && broker_pulse?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Maximum number of milliseconds allowed between bytes being sent by
|
32
|
+
# the client, or 0 if there is no limit. This method will add a 10% margin
|
33
|
+
# of error to the timeout determined from heartbeat negotiation to allow a
|
34
|
+
# little slack before a connection is deemed dead.
|
35
|
+
# @return [Fixnum]
|
36
|
+
def heartbeat_client_limit
|
37
|
+
unless defined?(@heartbeat_client_limit)
|
38
|
+
@heartbeat_client_limit = heartbeating[0] > 0 ? (1.1 * heartbeating[0]) : 0
|
39
|
+
end
|
40
|
+
@heartbeat_client_limit
|
41
|
+
end
|
42
|
+
|
43
|
+
# Maximum number of milliseconds allowed between bytes being sent from
|
44
|
+
# the broker, or 0 if there is no limit. This method will add a 10% margin
|
45
|
+
# of error to the timeout determined from heartbeat negotiation to allow a
|
46
|
+
# little slack before a connection is deemed dead.
|
47
|
+
# @return [Fixnum]
|
48
|
+
def heartbeat_broker_limit
|
49
|
+
unless defined?(@heartbeat_broker_limit)
|
50
|
+
@heartbeat_broker_limit = heartbeating[1] > 0 ? (1.1 * heartbeating[1]) : 0
|
51
|
+
end
|
52
|
+
@heartbeat_broker_limit
|
53
|
+
end
|
54
|
+
|
55
|
+
# Number of milliseconds since data was last transmitted to the broker or
|
56
|
+
# +nil+ if no data has been transmitted when the method is called.
|
57
|
+
# @return [Fixnum, nil]
|
58
|
+
def duration_since_transmitted
|
59
|
+
last_transmitted_at && ((Time.now - last_transmitted_at)*1000).to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
# Number of milliseconds since data was last received from the broker or
|
63
|
+
# +nil+ if no data has been received when the method is called.
|
64
|
+
# @return [Fixnum, nil]
|
65
|
+
def duration_since_received
|
66
|
+
last_received_at && ((Time.now - last_received_at)*1000).to_i
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if client-side heartbeating is disabled, or
|
70
|
+
# {#duration_since_transmitted} has not exceeded {#heartbeat_client_limit}
|
71
|
+
# @return [true, false]
|
72
|
+
def client_pulse?
|
73
|
+
heartbeat_client_limit == 0 || duration_since_transmitted <= heartbeat_client_limit
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns true if broker-side heartbeating is disabled, or
|
77
|
+
# {#duration_since_received} has not exceeded {#heartbeat_broker_limit}
|
78
|
+
# @return [true, false]
|
79
|
+
def broker_pulse?
|
80
|
+
heartbeat_broker_limit == 0 || duration_since_received <= heartbeat_broker_limit
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Classes that mix this in must define +split_header+ and +prepare_parsed_frame+
|
4
|
+
# The method +frame_to_string_base+ is provided as a factoring out of the
|
5
|
+
# common tasks of serializing a frame for STOMP 1.0 and STOMP 1.1.
|
6
|
+
module OnStomp::Connections::Serializers::Stomp_1
|
7
|
+
# Resets the parser that converts byte strings to {OnStomp::Components::Frame frames}
|
8
|
+
def reset_parser
|
9
|
+
@parse_accum = ''
|
10
|
+
@cur_frame = nil
|
11
|
+
@parse_state = :command
|
12
|
+
end
|
13
|
+
|
14
|
+
# The common elements of serializing a {OnStomp::Components::Frame frame}
|
15
|
+
# as a string in STOMP 1.0 and STOMP 1.1 protocols.
|
16
|
+
def frame_to_string_base frame
|
17
|
+
if frame.command
|
18
|
+
frame.force_content_length
|
19
|
+
str = "#{frame.command}\n"
|
20
|
+
frame.headers.inject(str) do |acc, (k,v)|
|
21
|
+
acc << yield(k,v)
|
22
|
+
end
|
23
|
+
str << "\n"
|
24
|
+
str << "#{frame.body}" if frame.body
|
25
|
+
str << "\000"
|
26
|
+
str
|
27
|
+
else
|
28
|
+
"\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Parses a {OnStomp::Components::Frame frame} command from the buffer
|
33
|
+
# @param [Array<String>] buffer
|
34
|
+
def parse_command buffer
|
35
|
+
data = buffer.shift
|
36
|
+
eol = data.index("\n")
|
37
|
+
if eol
|
38
|
+
parser_flush(buffer, data, eol, :finish_command)
|
39
|
+
else
|
40
|
+
@parse_accum << data
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parses a {OnStomp::Components::Frame frame} header line from the buffer
|
45
|
+
# @param [Array<String>] buffer
|
46
|
+
def parse_header_line buffer
|
47
|
+
data = buffer.shift
|
48
|
+
eol = data.index("\n")
|
49
|
+
if eol
|
50
|
+
parser_flush(buffer, data, eol, :finish_header_line)
|
51
|
+
else
|
52
|
+
@parse_accum << data
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Parses a {OnStomp::Components::Frame frame} body from the buffer
|
57
|
+
# @param [Array<String>] buffer
|
58
|
+
def parse_body buffer
|
59
|
+
data = buffer.shift
|
60
|
+
if rlen = @cur_frame.content_length
|
61
|
+
rlen -= @parse_accum.length
|
62
|
+
end
|
63
|
+
body_upto = rlen ? (rlen < data.length && rlen) : data.index("\000")
|
64
|
+
if body_upto
|
65
|
+
if data[body_upto, 1] != "\000"
|
66
|
+
raise OnStomp::MalformedFrameError, 'missing terminator'
|
67
|
+
end
|
68
|
+
parser_flush(buffer, data, body_upto, :finish_body)
|
69
|
+
else
|
70
|
+
@parse_accum << data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Adds the substring +data[0...idx]+ to the parser's accumulator,
|
75
|
+
# unshifts the remaining data back onto the buffer, and calls +meth+
|
76
|
+
# with the parser's accumulated string.
|
77
|
+
# @param [Array<String>] buffer
|
78
|
+
# @param [String] data
|
79
|
+
# @param [Fixnum] idx
|
80
|
+
# @param [Symbol] meth
|
81
|
+
def parser_flush buffer, data, idx, meth
|
82
|
+
remain = data[(idx+1)..-1]
|
83
|
+
buffer.unshift(remain) unless remain.empty?
|
84
|
+
__send__ meth, (@parse_accum + data[0...idx])
|
85
|
+
@parse_accum = ''
|
86
|
+
end
|
87
|
+
|
88
|
+
# Called when a frame's command has been fully read from the buffer. This
|
89
|
+
# method will create a new "current frame", set its
|
90
|
+
# {OnStomp::Components::Frame#command command} attribute, and tell the parser
|
91
|
+
# to move on to the next state.
|
92
|
+
# @param [String] command
|
93
|
+
def finish_command command
|
94
|
+
@cur_frame = OnStomp::Components::Frame.new
|
95
|
+
if command.empty?
|
96
|
+
@parse_state = :completed
|
97
|
+
else
|
98
|
+
@cur_frame.command = command
|
99
|
+
@parse_state = :header_line
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Called when a header line has been fully read from the buffer. This
|
104
|
+
# method will split the header line into a name/value pair,
|
105
|
+
# {OnStomp::Components::FrameHeaders#append append} the
|
106
|
+
# header to the "current frame" and tell the parser to move on to the next
|
107
|
+
# state
|
108
|
+
# @param [String] headline
|
109
|
+
def finish_header_line headline
|
110
|
+
if headline.empty?
|
111
|
+
@parse_state = :body
|
112
|
+
else
|
113
|
+
k,v = split_header(headline)
|
114
|
+
@cur_frame.headers.append(k, v)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Called when a frame's body has been fully read from the buffer. This
|
119
|
+
# method will set the frame's {OnStomp::Components::Frame#body body}
|
120
|
+
# attribute, call +prepare_parsed_frame+ with the "current frame",
|
121
|
+
# and tell the parser to move on to the next state.
|
122
|
+
# @param [String] body
|
123
|
+
def finish_body body
|
124
|
+
@cur_frame.body = body
|
125
|
+
prepare_parsed_frame @cur_frame
|
126
|
+
@parse_state = :completed
|
127
|
+
end
|
128
|
+
|
129
|
+
# Takes a buffer of strings and constructs all the
|
130
|
+
# {OnStomp::Components::Frame frames} it can from the data. The parser
|
131
|
+
# builds a "current frame" and updates it with attributes as they are
|
132
|
+
# parsed from the buffer. It is only safe to invoke this method from a
|
133
|
+
# single thread as no synchronization is being performed. This will work
|
134
|
+
# fine with the {OnStomp::Components::ThreadedProcessor threaded} processor
|
135
|
+
# as it performs its calls all within a single thread, but if you wish
|
136
|
+
# to develop your own processor that can call
|
137
|
+
# {OnStomp::Connections::Base#io_process_read} across separate threads, you
|
138
|
+
# will have to implement your own synchronization strategy.
|
139
|
+
# @note It is NOT safe to invoke this method from multiple threads as-is.
|
140
|
+
# @param [Array<String>] buffer
|
141
|
+
def bytes_to_frame buffer
|
142
|
+
until buffer.first.nil? && @parse_state != :completed
|
143
|
+
if @parse_state == :completed
|
144
|
+
yield @cur_frame
|
145
|
+
reset_parser
|
146
|
+
else
|
147
|
+
__send__(:"parse_#{@parse_state}", buffer)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if RUBY_VERSION >= "1.9"
|
153
|
+
# Takes the result of +frame_to_string+ and forces it to use a binary
|
154
|
+
# encoding
|
155
|
+
# @return [String] string serialization of frame with 'ASCII-8BIT' encoding
|
156
|
+
def frame_to_bytes frame
|
157
|
+
frame_to_string(frame).tap { |s| s.force_encoding('ASCII-8BIT') }
|
158
|
+
end
|
159
|
+
else
|
160
|
+
# Takes the result of +frame_to_string+ and passes it along. Ruby 1.8.7
|
161
|
+
# treats strings as a collection of bytes, so we don't need to do any
|
162
|
+
# further work.
|
163
|
+
# @return [String]
|
164
|
+
def frame_to_bytes(frame); frame_to_string(frame); end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Frame serializer / parser for STOMP 1.0 connections.
|
4
|
+
class OnStomp::Connections::Serializers::Stomp_1_0
|
5
|
+
include OnStomp::Connections::Serializers::Stomp_1
|
6
|
+
|
7
|
+
# Creates a new serializer and calls {#reset_parser}
|
8
|
+
def initialize
|
9
|
+
reset_parser
|
10
|
+
end
|
11
|
+
|
12
|
+
# Converts a {OnStomp::Components::Frame frame} to a string
|
13
|
+
# @param [OnStomp::Components::Frame] frame
|
14
|
+
# @return [String]
|
15
|
+
def frame_to_string frame
|
16
|
+
frame_to_string_base(frame) do |k,v|
|
17
|
+
"#{k.gsub(/[\n:]/, '')}:#{v.gsub(/\n/, '')}\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Splits a header line into a header name / header value pair at the first
|
22
|
+
# ':' character and returns the pair.
|
23
|
+
# @param [String] str header line to split
|
24
|
+
# @return [[String, String]]
|
25
|
+
# @raise [OnStomp::MalformedHeaderError] if the header line
|
26
|
+
# lacks a ':' character
|
27
|
+
def split_header(str)
|
28
|
+
col = str.index(':')
|
29
|
+
unless col
|
30
|
+
raise OnStomp::MalformedHeaderError, "unterminated header: '#{str}'"
|
31
|
+
end
|
32
|
+
[ str[0...col], str[(col+1)..-1] ]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Nothing special needs to be done with frames parsed from a STOMP 1.0
|
36
|
+
# connection, so this is a no-op.
|
37
|
+
# @param [OnStomp::Components::Frame] frame
|
38
|
+
# @return [nil]
|
39
|
+
def prepare_parsed_frame frame
|
40
|
+
end
|
41
|
+
end
|