stomp_out 0.1.0

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