stomper 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. data/.gitignore +5 -0
  2. data/{spec/spec.opts → .rspec} +0 -2
  3. data/Gemfile +4 -0
  4. data/LICENSE +201 -201
  5. data/README.md +130 -0
  6. data/Rakefile +5 -0
  7. data/examples/basic.rb +38 -0
  8. data/examples/events.rb +54 -0
  9. data/features/acking_messages.feature +147 -0
  10. data/features/disconnecting.feature +12 -0
  11. data/features/establish_connection.feature +44 -0
  12. data/features/protocol_version_negotiation.feature +61 -0
  13. data/features/receipts.feature +72 -0
  14. data/features/scopes.feature +32 -0
  15. data/features/secure_connections.feature +38 -0
  16. data/features/send_and_message.feature +28 -0
  17. data/features/steps/acking_messages_steps.rb +39 -0
  18. data/features/steps/disconnecting_steps.rb +8 -0
  19. data/features/steps/establish_connection_steps.rb +74 -0
  20. data/features/steps/frame_transmission_steps.rb +35 -0
  21. data/features/steps/protocol_version_negotiation_steps.rb +15 -0
  22. data/features/steps/receipts_steps.rb +79 -0
  23. data/features/steps/scopes_steps.rb +52 -0
  24. data/features/steps/secure_connections_steps.rb +41 -0
  25. data/features/steps/send_and_message_steps.rb +35 -0
  26. data/features/steps/subscribing_steps.rb +36 -0
  27. data/features/steps/threaded_receiver_steps.rb +8 -0
  28. data/features/steps/transactions_steps.rb +0 -0
  29. data/features/subscribing.feature +151 -0
  30. data/features/support/env.rb +11 -0
  31. data/features/support/header_helpers.rb +12 -0
  32. data/features/support/ssl/README +6 -0
  33. data/features/support/ssl/broker_cert.csr +17 -0
  34. data/features/support/ssl/broker_cert.pem +72 -0
  35. data/features/support/ssl/broker_key.pem +27 -0
  36. data/features/support/ssl/client_cert.csr +17 -0
  37. data/features/support/ssl/client_cert.pem +72 -0
  38. data/features/support/ssl/client_key.pem +27 -0
  39. data/features/support/ssl/demoCA/cacert.pem +17 -0
  40. data/features/support/ssl/demoCA/index.txt +2 -0
  41. data/features/support/ssl/demoCA/index.txt.attr +1 -0
  42. data/features/support/ssl/demoCA/index.txt.attr.old +1 -0
  43. data/features/support/ssl/demoCA/index.txt.old +1 -0
  44. data/features/support/ssl/demoCA/newcerts/01.pem +72 -0
  45. data/features/support/ssl/demoCA/newcerts/02.pem +72 -0
  46. data/features/support/ssl/demoCA/private/cakey.pem +17 -0
  47. data/features/support/ssl/demoCA/serial +1 -0
  48. data/features/support/ssl/demoCA/serial.old +1 -0
  49. data/features/support/test_stomp_server.rb +150 -0
  50. data/features/threaded_receiver.feature +11 -0
  51. data/features/transactions.feature +66 -0
  52. data/lib/stomper.rb +30 -20
  53. data/lib/stomper/connection.rb +442 -102
  54. data/lib/stomper/errors.rb +59 -0
  55. data/lib/stomper/extensions.rb +10 -0
  56. data/lib/stomper/extensions/common.rb +258 -0
  57. data/lib/stomper/extensions/events.rb +213 -0
  58. data/lib/stomper/extensions/heartbeat.rb +101 -0
  59. data/lib/stomper/extensions/scoping.rb +56 -0
  60. data/lib/stomper/frame.rb +54 -0
  61. data/lib/stomper/frame_serializer.rb +217 -0
  62. data/lib/stomper/headers.rb +15 -0
  63. data/lib/stomper/receipt_manager.rb +36 -0
  64. data/lib/stomper/receivers.rb +7 -0
  65. data/lib/stomper/receivers/threaded.rb +71 -0
  66. data/lib/stomper/scopes.rb +9 -0
  67. data/lib/stomper/scopes/header_scope.rb +49 -0
  68. data/lib/stomper/scopes/receipt_scope.rb +44 -0
  69. data/lib/stomper/scopes/transaction_scope.rb +109 -0
  70. data/lib/stomper/sockets.rb +66 -28
  71. data/lib/stomper/subscription_manager.rb +79 -0
  72. data/lib/stomper/support.rb +68 -0
  73. data/lib/stomper/support/1.8/frame_serializer.rb +53 -0
  74. data/lib/stomper/support/1.8/headers.rb +183 -0
  75. data/lib/stomper/support/1.9/frame_serializer.rb +64 -0
  76. data/lib/stomper/support/1.9/headers.rb +172 -0
  77. data/lib/stomper/support/ruby.rb +13 -0
  78. data/lib/stomper/uris.rb +49 -0
  79. data/lib/stomper/version.rb +7 -0
  80. data/spec/spec_helper.rb +13 -9
  81. data/spec/stomper/connection_spec.rb +712 -0
  82. data/spec/stomper/extensions/common_spec.rb +187 -0
  83. data/spec/stomper/extensions/events_spec.rb +78 -0
  84. data/spec/stomper/extensions/heartbeat_spec.rb +103 -0
  85. data/spec/stomper/extensions/scoping_spec.rb +21 -0
  86. data/spec/stomper/frame_serializer_1.8_spec.rb +318 -0
  87. data/spec/stomper/frame_serializer_spec.rb +316 -0
  88. data/spec/stomper/frame_spec.rb +36 -0
  89. data/spec/stomper/headers_spec.rb +224 -0
  90. data/spec/stomper/receipt_manager_spec.rb +91 -0
  91. data/spec/stomper/receivers/threaded_spec.rb +116 -0
  92. data/spec/stomper/scopes/header_scope_spec.rb +42 -0
  93. data/spec/stomper/scopes/receipt_scope_spec.rb +51 -0
  94. data/spec/stomper/scopes/transaction_scope_spec.rb +183 -0
  95. data/spec/stomper/sockets_spec.rb +113 -0
  96. data/spec/stomper/subscription_manager_spec.rb +107 -0
  97. data/spec/stomper/support_spec.rb +69 -0
  98. data/spec/stomper/uris_spec.rb +54 -0
  99. data/spec/stomper_spec.rb +9 -0
  100. data/spec/support/custom_argument_matchers.rb +57 -0
  101. data/spec/support/existential_frame_matchers.rb +19 -0
  102. data/spec/support/frame_header_matchers.rb +10 -0
  103. data/stomper.gemspec +30 -0
  104. metadata +272 -97
  105. data/AUTHORS +0 -21
  106. data/CHANGELOG +0 -20
  107. data/README.rdoc +0 -120
  108. data/lib/stomper/client.rb +0 -34
  109. data/lib/stomper/frame_reader.rb +0 -73
  110. data/lib/stomper/frame_writer.rb +0 -21
  111. data/lib/stomper/frames.rb +0 -39
  112. data/lib/stomper/frames/abort.rb +0 -10
  113. data/lib/stomper/frames/ack.rb +0 -25
  114. data/lib/stomper/frames/begin.rb +0 -11
  115. data/lib/stomper/frames/client_frame.rb +0 -89
  116. data/lib/stomper/frames/commit.rb +0 -10
  117. data/lib/stomper/frames/connect.rb +0 -10
  118. data/lib/stomper/frames/connected.rb +0 -30
  119. data/lib/stomper/frames/disconnect.rb +0 -10
  120. data/lib/stomper/frames/error.rb +0 -21
  121. data/lib/stomper/frames/message.rb +0 -48
  122. data/lib/stomper/frames/receipt.rb +0 -19
  123. data/lib/stomper/frames/send.rb +0 -10
  124. data/lib/stomper/frames/server_frame.rb +0 -38
  125. data/lib/stomper/frames/subscribe.rb +0 -42
  126. data/lib/stomper/frames/unsubscribe.rb +0 -19
  127. data/lib/stomper/open_uri_interface.rb +0 -41
  128. data/lib/stomper/receipt_handlers.rb +0 -23
  129. data/lib/stomper/receiptor.rb +0 -38
  130. data/lib/stomper/subscriber.rb +0 -76
  131. data/lib/stomper/subscription.rb +0 -128
  132. data/lib/stomper/subscriptions.rb +0 -95
  133. data/lib/stomper/threaded_receiver.rb +0 -59
  134. data/lib/stomper/transaction.rb +0 -185
  135. data/lib/stomper/transactor.rb +0 -50
  136. data/lib/stomper/uri.rb +0 -55
  137. data/spec/client_spec.rb +0 -29
  138. data/spec/connection_spec.rb +0 -22
  139. data/spec/frame_reader_spec.rb +0 -37
  140. data/spec/frame_writer_spec.rb +0 -27
  141. data/spec/frames/client_frame_spec.rb +0 -66
  142. data/spec/frames/indirect_frame_spec.rb +0 -45
  143. data/spec/frames/server_frame_spec.rb +0 -85
  144. data/spec/open_uri_interface_spec.rb +0 -132
  145. data/spec/receiptor_spec.rb +0 -35
  146. data/spec/shared_connection_examples.rb +0 -79
  147. data/spec/subscriber_spec.rb +0 -77
  148. data/spec/subscription_spec.rb +0 -157
  149. data/spec/subscriptions_spec.rb +0 -145
  150. data/spec/threaded_receiver_spec.rb +0 -33
  151. data/spec/transaction_spec.rb +0 -139
  152. data/spec/transactor_spec.rb +0 -46
data/lib/stomper.rb CHANGED
@@ -1,27 +1,37 @@
1
- require 'delegate'
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # For extensions to URI.parse for Stomp schemes.
2
4
  require 'uri'
3
- require 'io/wait'
5
+ # Primarily for CGI.parse
6
+ require 'cgi'
7
+ # Sockets are fairly important in all of this.
4
8
  require 'socket'
9
+ # As is openssl
10
+ require 'openssl'
11
+ # For IO#ready?
12
+ require 'io/wait'
13
+ # The socket helpers use this to delegate to the real sockets
14
+ require 'delegate'
15
+ # Threading and Mutex support
5
16
  require 'thread'
17
+ # Monitor support (prevent recursive dead locking)
6
18
  require 'monitor'
7
- require 'openssl'
8
- require 'stomper/uri'
9
- require 'stomper/frames'
10
- require 'stomper/frame_reader'
11
- require 'stomper/frame_writer'
12
- require 'stomper/sockets'
13
- require 'stomper/client'
14
- require 'stomper/transactor'
15
- require 'stomper/subscriber'
16
- require 'stomper/receiptor'
17
- require 'stomper/open_uri_interface'
18
- require 'stomper/threaded_receiver'
19
- require 'stomper/connection'
20
- require 'stomper/transaction'
21
- require 'stomper/subscription'
22
- require 'stomper/subscriptions'
23
- require 'stomper/receipt_handlers'
24
19
 
20
+ # Primary namespace of the stomper gem.
25
21
  module Stomper
26
- class MalformedFrameError < StandardError; end
27
22
  end
23
+
24
+ require 'stomper/version'
25
+ require 'stomper/errors'
26
+ require 'stomper/headers'
27
+ require 'stomper/sockets'
28
+ require 'stomper/frame'
29
+ require 'stomper/uris'
30
+ require 'stomper/frame_serializer'
31
+ require 'stomper/subscription_manager'
32
+ require 'stomper/receipt_manager'
33
+ require 'stomper/receivers'
34
+ require 'stomper/extensions'
35
+ require 'stomper/scopes'
36
+ require 'stomper/support'
37
+ require 'stomper/connection'
@@ -1,120 +1,460 @@
1
- module Stomper
2
- # A connection to a Stomp message broker. This class includes the Client,
3
- # Transactor, Subscriber, Receiptor and, in most cases, the ThreadedReceiver
4
- # modules.
5
- #
6
- # @see Client
7
- # @see Transactor
8
- # @see Subscriber
9
- # @see Receiptor
10
- # @see ThreadedReceiver
11
- class Connection
12
- attr_reader :uri
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # This class encapsulates a client connection to a message broker through the
4
+ # Stomp protocol. This class is also aliased as +Stomper::Client+
5
+ class Stomper::Connection
6
+ include ::Stomper::Extensions::Common
7
+ include ::Stomper::Extensions::Scoping
8
+ include ::Stomper::Extensions::Events
9
+ include ::Stomper::Extensions::Heartbeat
10
+
11
+ # The list of supported protocol versions
12
+ # @return [Array<String>]
13
+ PROTOCOL_VERSIONS = ['1.0', '1.1']
14
+
15
+ # The default configuration for connections. These settings have been
16
+ # deliberately left unfrozen to allow users to change defaults for all
17
+ # connections in one fell swoop.
18
+ # @return [{Symbol => Object}]
19
+ DEFAULT_CONFIG = {
20
+ :versions => ['1.0', '1.1'],
21
+ :heartbeats => [0, 0],
22
+ :host => nil,
23
+ :login => nil,
24
+ :passcode => nil,
25
+ :receiver_class => ::Stomper::Receivers::Threaded
26
+ }
13
27
 
14
- class << self
15
- def connect(uri, opts={})
16
- connex = new(uri, opts)
17
- connex.connect
18
- connex
28
+ # The URI representation of the broker this connection is associated with
29
+ # @return [URI]
30
+ attr_reader :uri
31
+
32
+ # The CONNECTED frame sent by the broker during the connection handshake.
33
+ # @return [Stomper::Frame,nil]
34
+ attr_reader :connected_frame
35
+
36
+ # The protocol versions to allow for this connection
37
+ # @return [Array<String>]
38
+ attr_reader :versions
39
+
40
+ # The protocol version negotiated between the client and broker. Will be
41
+ # +nil+ until the connection has been established.
42
+ # @return [String,nil]
43
+ attr_reader :version
44
+
45
+ # The client-side heartbeat settings to allow for this connection
46
+ # @return [Array<Fixnum>]
47
+ attr_reader :heartbeats
48
+
49
+ # The negotiated heartbeat strategy. The first element is the maximum
50
+ # number of milliseconds that the client can go without transmitting
51
+ # data or a heartbeat (a zero indicates that a client does not need to
52
+ # send heartbeats.) The second elemenet is the maximum number of milliseconds
53
+ # a server will go without transmitting data or a heartbeat (a zero indicates
54
+ # that the server need not send any heartbeats.)
55
+ # @return [Array<Fixnum>]
56
+ attr_reader :heartbeating
57
+
58
+ # The SSL options to use if this connection is secure
59
+ # @return [{Symbol => Object}, nil]
60
+ attr_reader :ssl
61
+
62
+ # The host header value to send to the broker when connecting. This allows
63
+ # the client to inform the server which host it wishes to connect with
64
+ # when multiple brokers may share an IP address through virtual hosting.
65
+ # @return [String]
66
+ attr_reader :host
67
+
68
+ # The login header value to send to the broker when connecting.
69
+ # @return [String]
70
+ attr_reader :login
71
+
72
+ # The passcode header value to send to the broker when connecting.
73
+ # @return [String]
74
+ attr_reader :passcode
75
+
76
+ # The class to use when instantiating a new receiver for the connection.
77
+ # Defaults to {Stomper::Receivers::Threaded}
78
+ # @return [CLass]
79
+ attr_reader :receiver_class
80
+
81
+ # A timestamp set to the last time a frame was transmitted. Returns +nil+
82
+ # if no frames have been transmitted yet
83
+ # @return [Time,nil]
84
+ attr_reader :last_transmitted_at
85
+
86
+ # A timestamp set to the last time a frame was received. Returns +nil+
87
+ # if no frames have been received yet
88
+ # @return [Time,nil]
89
+ attr_reader :last_received_at
90
+
91
+ # The subscription manager. Maintains the list of destinations subscribed
92
+ # to as well as the callbacks to invoke when a MESSAGE frame is received
93
+ # on one of them.
94
+ # @return [Stomper::SubscriptionManager]
95
+ attr_reader :subscription_manager
96
+
97
+ # The receipt manager. Maintains the list of receipt IDs and the callbacks
98
+ # associated with them that will be invoked when any frame with a matching
99
+ # +receipt-id+ header is received.
100
+ # @return [Stomper::ReceiptManager]
101
+ attr_reader :receipt_manager
102
+
103
+
104
+ # Creates a connection to a broker specified by the suppled uri. The given
105
+ # uri will be resolved to a URI instance through +URI.parse+. The final URI object must
106
+ # provide a {::URI::STOMP#create_socket create_socket} method, or an error
107
+ # will be raised. Both {::URI::STOMP} and {::URI::STOMP_SSL} provide this
108
+ # method, so string URIs with a scheme of either "stomp" or "stomp+ssl" will
109
+ # work automatically.
110
+ # Most connection options can be supplied through query parameters specified in the URI or
111
+ # through an optional +Hash+ parameter. If the same option is configured in
112
+ # both the URI's parameters and the options hash, the options hash takes
113
+ # precedence. Certain options, those pertaining to SSL settings for
114
+ # instance, must be configured through the options hash.
115
+ #
116
+ # @param [String] uri a string representing the URI to the message broker's
117
+ # Stomp interface.
118
+ # @param [{Symbol => Object}] options additional options for the connection.
119
+ # @option options [Array<String>] :versions (['1.0', '1.1']) protocol versions
120
+ # this connection should allow.
121
+ # @option options [Array<Fixnum>] :heartbeats ([0, 0]) heartbeat timings for
122
+ # this connection in milliseconds (a zero indicates that heartbeating is
123
+ # not desired from the client or the broker)
124
+ # @option options [{Symbol => Object}] :ssl ({}) SSL specific options to
125
+ # pass on when creating an {Stomper::Sockets::SSL SSL connection}.
126
+ # @option options [String] :host (nil) Host name to pass as +host+ header
127
+ # on CONNECT frames (will use actual connection hostname if not set)
128
+ # @option options [String] :login (nil) Username to send as +login+ header
129
+ # for credential authenticated connections.
130
+ # @option options [String] :passcode (nil) Password to send as +passcode+ header
131
+ # for credential authenticated connections.
132
+ #
133
+ # @example Connecting to a broker on 'host.domain.tld' and a port of 12345
134
+ # con = Stomper::Connection.new('stomp://host.domain.tld:12345')
135
+ #
136
+ # @example Connecting with login credentials
137
+ # con = Stomper::Connection.new('stomp://username:secret@host.domain.tld')
138
+ #
139
+ # @example Connecting using Stomp protocol 1.1, sending client beats once per second, and no interest in server beats.
140
+ # con = Stomper::Connection.new('stomp://host/?versions=1.1&heartbeats=1000&heartbeats=0')
141
+ # con = Stomper::Connection.new('stomp://host', :versions => '1.1', :heartbeats => [1000, 0])
142
+ # # both result in:
143
+ # con.heartbeat #=> [1000, 0]
144
+ # con.versions #=> ['1.1']
145
+ #
146
+ # @example Repeated options in URI and options hash
147
+ # con = Stomper::Connection.new('stomp://host?versions=1.1&versions=1.0', :versions => '1.1')
148
+ # con.versions #=> '1.1'
149
+ # # In this case, the versions query parameter value +[1.1 , 1.0]+ is
150
+ # # overridden by the options hash setting +1.1+
151
+ def initialize(uri, options={})
152
+ @ssl = options.delete(:ssl) || {}
153
+ @uri = uri.is_a?(::URI) ? uri : ::URI.parse(uri)
154
+ config = ::Stomper::Support.keys_to_sym(::CGI.parse(@uri.query || '')).
155
+ merge(::Stomper::Support.keys_to_sym(options))
156
+ DEFAULT_CONFIG.each do |attr_name, def_val|
157
+ if config.key? attr_name
158
+ __send__ :"#{attr_name}=", config[attr_name]
159
+ elsif def_val
160
+ __send__ :"#{attr_name}=", def_val
19
161
  end
20
- alias_method :open, :connect
21
162
  end
163
+ @host ||= (@uri.host||'localhost')
164
+ @login ||= (@uri.user || '')
165
+ @passcode ||= (@uri.password || '')
166
+ @connected = false
167
+ @heartbeating = [0,0]
168
+ @last_transmitted_at = @last_received_at = nil
169
+ @subscription_manager = ::Stomper::SubscriptionManager.new(self)
170
+ @receipt_manager = ::Stomper::ReceiptManager.new(self)
171
+ @connecting = false
172
+ @disconnecting = false
173
+
174
+ on_connected do |cf, con|
175
+ unless connected?
176
+ @connecting = false
177
+ @disconnecting = false
178
+ @version = (cf[:version].nil?||cf[:version].empty?) ? '1.0' : cf[:version]
179
+ unless @versions.include?(@version)
180
+ close
181
+ raise ::Stomper::Errors::UnsupportedProtocolVersionError,
182
+ "broker requested '#{@version}', client allows: #{@versions.inspect}"
183
+ end
184
+ c_x, c_y = @heartbeats
185
+ s_x, s_y = (cf[:'heart-beat'] || '0,0').split(',').map do |v|
186
+ vi = v.to_i
187
+ vi > 0 ? vi : 0
188
+ end
189
+ @heartbeating = [ (c_x == 0||s_y == 0 ? 0 : [c_x,s_y].max),
190
+ (c_y == 0||s_x == 0 ? 0 : [c_y,s_x].max) ]
22
191
 
23
- # Creates a new connection to the Stomp broker specified by +uri+.
24
- # The +uri+ parameter may be either a URI object, or something that can
25
- # be parsed by URI.parse, such as a string.
26
- # Some examples of acceptable +uri+ forms include:
27
- # [stomp:///] Connection will be made to 'localhost' on port 61613 with no login credentials.
28
- # [stomp+ssl:///] Same as above, but connection will be made on port 61612 and wrapped by SSL.
29
- # [stomp://user:pass@host.tld] Connection will be made to 'host.tld', authenticating as 'user' with a password of 'pass'.
30
- # [stomp://user:pass@host.tld:86753] Connection will be made to 'host.tld' on port 86753, authenticating as above.
31
- # [stomp://host.tld:86753] Connection will be made to 'host.tld' on port 86753, with no authentication.
32
- #
33
- # In order to wrap the connection with SSL, the schema of +uri+ must be 'stomp+ssl';
34
- # however, if SSL is not required, the schema is essentially ignored.
35
- # The default port for the 'stomp+ssl' schema is 61612, all other schemas
36
- # default to port 61613.
37
- def initialize(uri, opts={})
38
- if opts.delete(:threaded_receiver) { true }
39
- extend ::Stomper::ThreadedReceiver
192
+ extend_for_protocol
40
193
  end
41
- @uri = (uri.is_a?(URI) && uri) || URI.parse(uri)
42
- raise ArgumentError, 'Expected URI schema to be one of stomp or stomp+ssl' unless @uri.respond_to?(:create_socket)
43
- @connected = false
44
- @writer = @reader = nil
45
194
  end
46
-
47
-
48
- # Connects to the broker specified by the +uri+ attribute.
49
- # By default, this method is invoked when a new Stomper::Connection
50
- # is created.
51
- #
52
- # See also: new
53
- def connect
54
- @connected = false
55
- @socket = @uri.create_socket
56
- transmit Stomper::Frames::Connect.new(@uri.user, @uri.password)
57
- @connected = receive.instance_of?(Stomper::Frames::Connected)
195
+
196
+ on_disconnect do |df, con|
197
+ @disconnecting = true
198
+ close unless df[:receipt]
58
199
  end
59
-
60
- # Returns true when there is an open connection
61
- # established to the broker.
62
- def connected?
63
- @connected && @socket && !@socket.closed?
200
+ end
201
+
202
+ # Sets the protocol versions that are acceptable for this connection.
203
+ # This method accepts version numbers as:
204
+ # - A single string value (eg: '1.0')
205
+ # - An array of string values (eg: ['1.1', '1.0'])
206
+ # @overload versions=(version)
207
+ # Sets the acceptable versions to a single supplied version.
208
+ # @param [String] version the protocol version to accept
209
+ # @overload versions=(versions)
210
+ # Sets the acceptable versions to the list provided
211
+ # @param [Array<String>] versions list of acceptable protocol versions
212
+ # @return [Array<String>] acceptable protocol versions
213
+ # @raise [Stomper::Errors::UnsupportedProtocolVersionError] if none of the
214
+ # versions provided are supported by this library
215
+ def versions=(vers)
216
+ vers = [vers] unless vers.is_a?(Array)
217
+ @versions = PROTOCOL_VERSIONS.select { |v| vers.include? v }
218
+ if @versions.empty?
219
+ raise ::Stomper::Errors::UnsupportedProtocolVersionError, "no supported protocol versions in #{vers.inspect}"
64
220
  end
65
-
66
- # Transmits a Stomper::Frames::Disconnect frame to the broker
67
- # then terminates the connection by invoking +close+.
68
- def disconnect
69
- begin
70
- @connected = false
71
- transmit(Stomper::Frames::Disconnect.new)
72
- ensure
73
- close_socket
221
+ @versions
222
+ end
223
+
224
+ # Sets the client-side heartbeat settings to allow for this connection.
225
+ # The first element specifies the smallest number of milliseconds between
226
+ # heartbeats the client can guarantee, a value of 0 indicates that the client
227
+ # cannot send heartbeats. The second element specifies the
228
+ # duration in milliseconds between heartbeats that the client would like
229
+ # to receive from the server, a value of 0 indicates that the client does not
230
+ # want to receive server heartbeats. Both values are converted to integers,
231
+ # and negative values are replaced with 0.
232
+ #
233
+ # @param [Array<Fixnum>] beats
234
+ def heartbeats=(beats)
235
+ @heartbeats = beats[0..1].map { |b| bi = b.to_i; bi > 0 ? bi : 0 }
236
+ end
237
+
238
+ # Sets the host header value to use when connecting to the server. This
239
+ # provides the client with the ability to specify a specific broker that
240
+ # resides on a server that supports virtual hosts.
241
+ #
242
+ # @param [String] val
243
+ def host=(val); @host = (val.is_a?(Array) ? val.first : val).to_s; end
244
+
245
+ # Sets the login header value to use when connecting to the server.
246
+ #
247
+ # @param [String] val
248
+ def login=(val); @login = (val.is_a?(Array) ? val.first : val).to_s; end
249
+
250
+ # Sets the passcode header value to use when connecting to the server.
251
+ #
252
+ # @param [String] val
253
+ def passcode=(val)
254
+ @passcode = (val.is_a?(Array) ? val.first : val).to_s
255
+ end
256
+
257
+ # Sets the class to use when a receiver needs to be created
258
+ # @see #start
259
+ # @see #stop
260
+ # @see Stomper::Receivers::Threaded
261
+ def receiver_class=(val)
262
+ @receiver_class = ::Stomper::Support.constantize(val.is_a?(Array) ?
263
+ val.first : val)
264
+ end
265
+
266
+ # Establishes a connection to the broker. After the socket connection is
267
+ # established, a CONNECT/STOMP frame will be sent to the broker and a frame
268
+ # will be read from the TCP stream. If the frame is a CONNECTED frame, the
269
+ # connection has been established and you're ready to go, otherwise the
270
+ # socket will be closed and an error will be raised.
271
+ def connect(headers={})
272
+ @socket = @uri.create_socket(@ssl)
273
+ @serializer = ::Stomper::FrameSerializer.new(@socket)
274
+ m_headers = {
275
+ :'accept-version' => @versions.join(','),
276
+ :host => @host,
277
+ :'heart-beat' => @heartbeats.join(','),
278
+ :login => @login,
279
+ :passcode => @passcode
280
+ }
281
+ @connecting = true
282
+ transmit create_frame('CONNECT', headers, m_headers)
283
+ receive.tap do |f|
284
+ if f.command == 'CONNECTED'
285
+ @connected_frame = f
286
+ else
287
+ close
288
+ raise ::Stomper::Errors::ConnectFailedError, 'broker did not send CONNECTED frame'
74
289
  end
75
290
  end
76
-
77
- alias_method :close, :disconnect
78
-
79
- # Transmits the a Stomp Frame to the connected Stomp broker by
80
- # way of an internal FrameWriter. If an exception is raised
81
- # during the transmission, the connection will be forcibly closed
82
- # and the exception will be propegated.
83
- def transmit(frame)
84
- begin
85
- @socket.transmit_frame(frame)
86
- frame
87
- rescue Exception => ioerr
88
- close_socket :lost_connection
89
- raise ioerr
291
+ @connected = true
292
+ trigger_event(:on_connection_established, self) if @connected
293
+ @connected_frame
294
+ end
295
+ alias :open :connect
296
+
297
+ class << self
298
+ # Creates a new connection and immediately connects it to the broker.
299
+ # @see #initialize
300
+ def connect(uri, options={})
301
+ conx = new(uri, options)
302
+ conx.connect
303
+ conx
304
+ end
305
+ alias :open :connect
306
+ end
307
+
308
+ # True if a connection with the broker has been established, false otherwise.
309
+ # @return [true,false]
310
+ def connected?
311
+ @connected && !@socket.closed?
312
+ end
313
+
314
+ # Creates an instance of the class given by {#receiver_class} and starts it.
315
+ # A call to {#connect} will be made if the connection has not been established.
316
+ # The class to instantiate can be overridden on a per connection basis, or
317
+ # for all connections by changing DEFAULT_CONFIG[:receiver_class]
318
+ # @param [{Object => String}] headers optional headers to pass to {#connect}
319
+ # if the connection has not yet been established.
320
+ # @return [self]
321
+ # @see #stop
322
+ # @see #connect
323
+ def start(headers={})
324
+ connect(headers) unless @connected
325
+ @receiver ||= receiver_class.new(self)
326
+ @receiver.start
327
+ self
328
+ end
329
+
330
+ # Stops the instantiated receiver and calls {#disconnect} if a connection
331
+ # has been established.
332
+ # @param [{Object => String}] headers optional headers to pass to {#disconnect}
333
+ # if the connection has been established.
334
+ # @return [self]
335
+ # @raise [Exception] if invoking +stop+ on the receiver raises an exception
336
+ # @see #start
337
+ # @see #disconnect
338
+ # @see Stomper::Receivers::Threaded#stop for an example of when a receiver
339
+ # may raise an exception when stopped.
340
+ def stop(headers={})
341
+ disconnect(headers) unless @disconnecting
342
+ @receiver && @receiver.stop
343
+ self
344
+ end
345
+
346
+ # Returns true if the receiver exists and is running.
347
+ def running?
348
+ @receiver && @receiver.running?
349
+ end
350
+
351
+ # Disconnects from the broker immediately. This is not a polite disconnect,
352
+ # meaning that no DISCONNECT frame is transmitted to the broker, the socket
353
+ # is shutdown and closed immediately. Calls to {#disconnect} invoke this
354
+ # method internally after the DISCONNECT frame has been transmitted. This
355
+ # method always triggers the
356
+ # {Stomper::Extensions::Events#on_connection_closed on_connection_closed} event
357
+ # and if +true+ is passed as a parameter,
358
+ # {Stomper::Extensions::Events#on_connection_terminated on_connection_terminated}
359
+ # will be triggered as well.
360
+ # @see #disconnect
361
+ # @see Stomper::Extensions::Events#on_connection_closed
362
+ # @see Stomper::Extensions::Events#on_connection_terminated
363
+ # @param [true,false] fire_terminated (false) If true, trigger
364
+ # {Stomper::Extensions::Events#on_connection_terminated}
365
+ def close(fire_terminated=false)
366
+ begin
367
+ trigger_event(:on_connection_terminated, self) if fire_terminated
368
+ ensure
369
+ unless @socket.closed?
370
+ @socket.shutdown(2) rescue nil
371
+ @socket.close rescue nil
90
372
  end
373
+ @connected = false
91
374
  end
92
-
93
- # Receives the next Stomp Frame from an internal FrameReader that
94
- # wraps the underlying Stomp broker connection. If an exception is raised
95
- # during the fetch, the connection will be forcibly closed and the exception
96
- # will be propegated.
97
- def receive()
375
+ trigger_event(:on_connection_closed, self)
376
+ end
377
+
378
+ # Transmits a frame to the broker. This is a low-level method used internally
379
+ # by the more user friendly interface.
380
+ # @param [Stomper::Frame] frame
381
+ def transmit(frame)
382
+ trigger_event(:on_connection_died, self) if dead?
383
+ trigger_event(:before_transmitting, frame, self)
384
+ trigger_before_transmitted_frame(frame, self)
385
+ begin
386
+ @serializer.write_frame(frame).tap do
387
+ @last_transmitted_at = Time.now
388
+ trigger_event(:after_transmitting, frame, self)
389
+ trigger_transmitted_frame(frame, self)
390
+ end
391
+ rescue ::IOError, ::SystemCallError
392
+ close(true)
393
+ raise
394
+ end
395
+ end
396
+
397
+ # Receives a frame from the broker.
398
+ # @return [Stomper::Frame]
399
+ def receive
400
+ trigger_event(:on_connection_died, self) if dead?
401
+ if alive? || @connecting
402
+ trigger_event(:before_receiving, nil, self)
98
403
  begin
99
- @socket.receive_frame
100
- rescue Exception => ioerr
101
- close_socket :lost_connection
102
- raise ioerr
404
+ @serializer.read_frame.tap do |f|
405
+ if f.nil?
406
+ close(true) if @connected
407
+ else
408
+ @last_received_at = Time.now
409
+ trigger_event(:after_receiving, f, self)
410
+ trigger_received_frame(f, self)
411
+ end
412
+ end
413
+ rescue ::IOError, ::SystemCallError
414
+ close(true)
415
+ raise
103
416
  end
104
417
  end
105
-
106
- # Immediately closes the connection to the broker, without the
107
- # formality of sending a Disconnect frame.
108
- #
109
- # See also: disconnect
110
- def close_socket(conx_state = :disconnected)
111
- @socket.shutdown(2)
112
- @socket.close if @socket
418
+ end
419
+
420
+ # Receives a frame from the broker if there is data to be read from the
421
+ # underlying socket. If there is no data available for reading from the
422
+ # socket, +nil+ is returned.
423
+ # @note While this method will not block if there is no data ready for reading,
424
+ # if any data is available it will block until a complete frame has been read.
425
+ # @return [Stomper::Frame, nil]
426
+ def receive_nonblock
427
+ trigger_event(:on_connection_died, self) if dead?
428
+ trigger_event(:before_receiving, self)
429
+ if @socket.ready?
430
+ @serializer.read_frame.tap do |f|
431
+ trigger_event(:after_receiving, self, f)
432
+ trigger_received_frame(f, self)
433
+ end
113
434
  end
114
-
115
- include ::Stomper::Client
116
- include ::Stomper::Transactor
117
- include ::Stomper::Subscriber
118
- include ::Stomper::Receiptor
435
+ end
436
+
437
+ # Duration in milliseconds since a frame has been transmitted to the broker.
438
+ # @return [Fixnum]
439
+ def duration_since_transmitted
440
+ @last_transmitted_at && ((Time.now - @last_transmitted_at)*1000).to_i
441
+ end
442
+
443
+ # Duration in milliseconds since a frame has been received from the broker.
444
+ # @return [Fixnum]
445
+ def duration_since_received
446
+ @last_received_at && ((Time.now - @last_received_at)*1000).to_i
447
+ end
448
+
449
+ private
450
+ def extend_for_protocol
451
+ ::Stomper::Extensions::Common.extend_by_protocol_version(self, @version)
452
+ ::Stomper::Extensions::Heartbeat.extend_by_protocol_version(self, @version)
453
+ @serializer.extend_for_protocol @version
454
+ self
119
455
  end
120
456
  end
457
+
458
+ # Alias Stomper::Client to Stomper::Connection
459
+ ::Stomper::Client = ::Stomper::Connection
460
+