onstomp 1.0.0pre1
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.
- 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
|