stomp_out 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2015 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module StompOut
23
+
24
+ # Container for STOMP frame
25
+ class Frame
26
+
27
+ attr_accessor :command, :headers, :body
28
+
29
+ # Create Stomp frame
30
+ #
31
+ # @param [String, NilClass] command name in upper case
32
+ # @param [Hash, NilClass] headers with string header name as key
33
+ # @param [String] body
34
+ def initialize(command = nil, headers = nil, body = nil)
35
+ @command = command
36
+ @headers = headers || {}
37
+ @body = body || ""
38
+ end
39
+
40
+ # Serialize frame for transmission on wire
41
+ #
42
+ # @return [String] serialized frame
43
+ def to_s
44
+ @headers["content-length"] = @body.size.to_s if @body.include?(NULL)
45
+ @headers.keys.sort.inject("#{@command}\n") { |r, key| r << "#{key}:#{@headers[key]}\n" } + "\n#{@body}#{NULL}\n"
46
+ end
47
+
48
+ # Verify that required headers are present and then return their values
49
+ #
50
+ # @param [String] version of STOMP in use
51
+ # @param [Hash] required headers with name as key and list of STOMP versions
52
+ # to be excluded from the verification as value
53
+ #
54
+ # @return [Array, Object] values of selected headers in header name sorted order,
55
+ # or individual header value if only one header required
56
+ #
57
+ # @raise [ProtocolError] missing header
58
+ def require(version, required)
59
+ values = []
60
+ required.keys.sort.each do |header|
61
+ exclude = required[header]
62
+ value = @headers[header]
63
+ raise ProtocolError.new("Missing '#{header}' header", self) if value.nil? && !exclude.include?(version)
64
+ values << value
65
+ end
66
+ values.size > 1 ? values : values.first
67
+ end
68
+
69
+ end # Frame
70
+
71
+ end # StompOut
@@ -0,0 +1,151 @@
1
+ # Copyright (c) 2015 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module StompOut
23
+
24
+ class Heartbeat
25
+
26
+ # EOL that acts as STOMP heartbeat
27
+ HEARTBEAT = "\n"
28
+
29
+ # Error margin for receiving heartbeats
30
+ ERROR_MARGIN_FACTOR = 1.5
31
+
32
+ attr_reader :incoming_rate, :outgoing_rate
33
+
34
+ # Determine whether heartbeat is usable based on whether eventmachine is available
35
+ #
36
+ # @return [Boolean] whether usable
37
+ def self.usable?
38
+ require 'eventmachine'
39
+ true
40
+ rescue LoadError => e
41
+ false
42
+ end
43
+
44
+ # Analyze heartbeat request and if there is an agreeable rate start generating
45
+ # heartbeats and/or monitoring incoming heartbeats
46
+ #
47
+ # @param [Client, Server] stomp client/server requesting heartbeat service that responds
48
+ # to the following callbacks:
49
+ # send_data(data) - send data over connection
50
+ # report_error(error) - report error to user
51
+ # @param [String, NilClass] rate_requested requested with two positive integers separated
52
+ # by comma; first integer indicates far end's support for sending heartbeats with 0 meaning
53
+ # cannot send and any other value indicating number of milliseconds between heartbeats
54
+ # it can guarantee; second integer indicates the heartbeats the far end would like
55
+ # to receive with 0 meaning none and any other value indicating the desired number
56
+ # of milliseconds between heartbeats
57
+ # @param [Integer] min_send_interval in msec that near end is willing to guarantee
58
+ # @param [Integer] desired_receive_interval in msec for far end to send heartbeats
59
+ #
60
+ # @raise [ProtocolError] invalid heartbeat setting
61
+ def initialize(stomp, rate_requested, min_send_interval = 0, desired_receive_interval = 0)
62
+ @stomp = stomp
63
+ @received_data = @sent_data = false
64
+ @incoming_rate = @outgoing_rate = 0
65
+ if rate_requested
66
+ @incoming_rate, @outgoing_rate = rate_requested.split(",").map do |h|
67
+ raise StompOut::ProtocolError, "Invalid 'heart-beat' header" if h.nil? || h !~ /^\d+$/
68
+ h.to_i
69
+ end
70
+ raise StompOut::ProtocolError, "Invalid 'heart-beat' header" if @outgoing_rate.nil? || @incoming_rate.nil?
71
+ @incoming_rate = [@incoming_rate, min_send_interval].max if @incoming_rate > 0
72
+ @outgoing_rate = [@outgoing_rate, desired_receive_interval].max if @outgoing_rate > 0
73
+ end
74
+ end
75
+
76
+ # Start heartbeat service
77
+ #
78
+ # @return [TrueClass] always true
79
+ def start
80
+ monitor_incoming if @incoming_rate > 0
81
+ generate_outgoing if @outgoing_rate > 0
82
+ end
83
+
84
+ # Stop heartbeat service
85
+ #
86
+ # @return [TrueClass] always true
87
+ def stop
88
+ if @incoming_timer
89
+ @incoming_timer.cancel
90
+ @incoming_timer = nil
91
+ end
92
+ if @outgoing_timer
93
+ @outgoing_timer.cancel
94
+ @outgoing_timer = nil
95
+ end
96
+ true
97
+ end
98
+
99
+ # Record that data has been sent to far end
100
+ #
101
+ # @return [TrueClass] always true
102
+ def sent_data
103
+ @sent_data = true
104
+ end
105
+
106
+ # Record that data has been received from far end
107
+ #
108
+ # @return [TrueClass] always true
109
+ def received_data
110
+ @received_data = true
111
+ end
112
+
113
+ protected
114
+
115
+ # Monitor incoming heartbeats
116
+ # Report failure and stop heartbeat if miss heartbeat by more than
117
+ # specified margin
118
+ #
119
+ # @return [TrueClass] always true
120
+ def monitor_incoming
121
+ interval = (@incoming_rate * ERROR_MARGIN_FACTOR) / 1000.0
122
+ @incoming_timer = EM::PeriodicTimer.new(interval) do
123
+ if @received_data
124
+ @received_data = false
125
+ else
126
+ stop
127
+ @stomp.report_error("heartbeat failure")
128
+ end
129
+ end
130
+ true
131
+ end
132
+
133
+ # Generate outgoing heartbeats whenever there is not any other
134
+ # send activity for given heartbeat interval
135
+ #
136
+ # @return [TrueClass] always true
137
+ def generate_outgoing
138
+ interval = @outgoing_rate / 1000.0
139
+ @outgoing_timer = EM::PeriodicTimer.new(interval) do
140
+ if @sent_data
141
+ @sent_data = false
142
+ else
143
+ @stomp.send_data(HEARTBEAT)
144
+ @sent_data = true
145
+ end
146
+ end
147
+ end
148
+
149
+ end # Heartbeat
150
+
151
+ end # StompOut
@@ -0,0 +1,134 @@
1
+ # Copyright (c) 2015 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module StompOut
23
+
24
+ # Null terminator for frame
25
+ NULL = "\000"
26
+
27
+ # Parser for converting stream of data from connection into STOMP frames
28
+ class Parser
29
+
30
+ # Create frame parser
31
+ def initialize
32
+ @buffer = ""
33
+ @body_length = nil
34
+ @frame = Frame.new
35
+ @frames = []
36
+ end
37
+
38
+ # Add data received from connection to end of buffer
39
+ #
40
+ # @return [TrueClass] always true
41
+ def <<(buf)
42
+ @buffer << buf
43
+ parse
44
+ end
45
+
46
+ # Get next frame
47
+ #
48
+ # @return [Frame, NilClass] frame or nil if none available
49
+ def next
50
+ @frames.shift
51
+ end
52
+
53
+ protected
54
+
55
+ # Parse the contents of the buffer
56
+ #
57
+ # @return [TrueClass] always true
58
+ def parse
59
+ parse_command_and_headers if @frame.command.nil?
60
+ success = if @frame.command
61
+ if @body_length
62
+ parse_binary_body
63
+ else
64
+ parse_text_body
65
+ end
66
+ elsif (match = @buffer.match(/\A\n|\A\r|\A\r\n/))
67
+ # Ignore heartbeat
68
+ @buffer = match.post_match
69
+ true
70
+ end
71
+
72
+ # Keep parsing if making progress and there is more data
73
+ parse if success && !@buffer.empty?
74
+ true
75
+ end
76
+
77
+ # Parse next command and headers at beginning of buffer
78
+ #
79
+ # @return [TrueClass] always true
80
+ def parse_command_and_headers
81
+ if (match = @buffer.match(/\A\s*(\S+)\r?\n((?:[ \t]*.*?[ \t]*:[ \t]*.*?[ \t]*$\r?\n)*)\r?\n/))
82
+ @frame.command, headers = match.captures
83
+ @buffer = match.post_match
84
+ headers.split(/\r?\n/).each do |data|
85
+ if data.match(/^\s*(\S+)\s*:\s*(.*?\s*)$/)
86
+ @frame.headers[$1] = $2 unless @frame.headers.has_key?($1)
87
+ end
88
+ end
89
+ @body_length = (length = @frame.headers["content-length"]) && length.to_i
90
+ elsif @buffer.rindex(NULL)
91
+ raise ProtocolError, "Invalid frame (malformed headers)"
92
+ end
93
+ true
94
+ end
95
+
96
+ # Parse binary body at beginning of buffer
97
+ #
98
+ # @return [Frame, NilClass] frame created or nil if need more data
99
+ #
100
+ # @raise [ProtocolError] missing frame null terminator
101
+ def parse_binary_body
102
+ if @buffer.size > @body_length
103
+ # Also test for 0 here to be compatible with Ruby 1.8 string handling
104
+ unless [NULL, 0].include?(@buffer[@body_length])
105
+ raise ProtocolError, "Invalid frame (missing null terminator)"
106
+ end
107
+ parse_body(@body_length)
108
+ end
109
+ end
110
+
111
+ # Parse text body at beginning of buffer
112
+ #
113
+ # @return [Frame, NilClass] frame created or nil if need more data
114
+ def parse_text_body
115
+ if (length = @buffer.index(NULL))
116
+ parse_body(length)
117
+ end
118
+ end
119
+
120
+ # Parse body at beginning of buffer to complete frame
121
+ #
122
+ # @param [Integer] length of body
123
+ #
124
+ # @return [Frame] new frame
125
+ def parse_body(length)
126
+ @frame.body = @buffer[0...length]
127
+ @buffer = @buffer[length+1..-1]
128
+ @frames << @frame
129
+ @frame = Frame.new
130
+ end
131
+
132
+ end # Parser
133
+
134
+ end # StompOut