garaio_bunny 2.19.1

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +231 -0
  3. data/lib/amq/protocol/extensions.rb +16 -0
  4. data/lib/bunny/authentication/credentials_encoder.rb +55 -0
  5. data/lib/bunny/authentication/external_mechanism_encoder.rb +27 -0
  6. data/lib/bunny/authentication/plain_mechanism_encoder.rb +19 -0
  7. data/lib/bunny/channel.rb +2055 -0
  8. data/lib/bunny/channel_id_allocator.rb +82 -0
  9. data/lib/bunny/concurrent/atomic_fixnum.rb +75 -0
  10. data/lib/bunny/concurrent/condition.rb +66 -0
  11. data/lib/bunny/concurrent/continuation_queue.rb +62 -0
  12. data/lib/bunny/concurrent/linked_continuation_queue.rb +61 -0
  13. data/lib/bunny/concurrent/synchronized_sorted_set.rb +56 -0
  14. data/lib/bunny/consumer.rb +128 -0
  15. data/lib/bunny/consumer_tag_generator.rb +23 -0
  16. data/lib/bunny/consumer_work_pool.rb +122 -0
  17. data/lib/bunny/cruby/socket.rb +110 -0
  18. data/lib/bunny/cruby/ssl_socket.rb +118 -0
  19. data/lib/bunny/delivery_info.rb +93 -0
  20. data/lib/bunny/exceptions.rb +269 -0
  21. data/lib/bunny/exchange.rb +275 -0
  22. data/lib/bunny/framing.rb +56 -0
  23. data/lib/bunny/get_response.rb +83 -0
  24. data/lib/bunny/heartbeat_sender.rb +71 -0
  25. data/lib/bunny/jruby/socket.rb +57 -0
  26. data/lib/bunny/jruby/ssl_socket.rb +58 -0
  27. data/lib/bunny/message_properties.rb +119 -0
  28. data/lib/bunny/queue.rb +393 -0
  29. data/lib/bunny/reader_loop.rb +158 -0
  30. data/lib/bunny/return_info.rb +74 -0
  31. data/lib/bunny/session.rb +1483 -0
  32. data/lib/bunny/socket.rb +14 -0
  33. data/lib/bunny/ssl_socket.rb +14 -0
  34. data/lib/bunny/test_kit.rb +41 -0
  35. data/lib/bunny/timeout.rb +7 -0
  36. data/lib/bunny/transport.rb +526 -0
  37. data/lib/bunny/version.rb +6 -0
  38. data/lib/bunny/versioned_delivery_tag.rb +28 -0
  39. data/lib/bunny.rb +92 -0
  40. metadata +127 -0
@@ -0,0 +1,275 @@
1
+ require 'amq/protocol'
2
+
3
+ module Bunny
4
+ # Represents AMQP 0.9.1 exchanges.
5
+ #
6
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
7
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
8
+ class Exchange
9
+
10
+ #
11
+ # API
12
+ #
13
+
14
+ # @return [Bunny::Channel]
15
+ attr_reader :channel
16
+
17
+ # @return [String]
18
+ attr_reader :name
19
+
20
+ # Type of this exchange (one of: :direct, :fanout, :topic, :headers).
21
+ # @return [Symbol]
22
+ attr_reader :type
23
+
24
+ # @return [Symbol]
25
+ # @api plugin
26
+ attr_reader :status
27
+
28
+ # Options hash this exchange instance was instantiated with
29
+ # @return [Hash]
30
+ attr_accessor :opts
31
+
32
+
33
+ # The default exchange. This exchange is a direct exchange that is predefined by the broker
34
+ # and that cannot be removed. Every queue is bound to this exchange by default with
35
+ # the following routing semantics: messages will be routed to the queue with the same
36
+ # name as the message's routing key. In other words, if a message is published with
37
+ # a routing key of "weather.usa.ca.sandiego" and there is a queue with this name,
38
+ # the message will be routed to the queue.
39
+ #
40
+ # @param [Bunny::Channel] channel_or_connection Channel to use. {Bunny::Session} instances
41
+ # are only supported for backwards compatibility.
42
+ #
43
+ # @example Publishing a messages to the tasks queue
44
+ # channel = Bunny::Channel.new(connection)
45
+ # tasks_queue = channel.queue("tasks")
46
+ # Bunny::Exchange.default(channel).publish("make clean", :routing_key => "tasks")
47
+ #
48
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
49
+ # @see http://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.2.4)
50
+ # @note Do not confuse the default exchange with amq.direct: amq.direct is a pre-defined direct
51
+ # exchange that doesn't have any special routing semantics.
52
+ # @return [Exchange] An instance that corresponds to the default exchange (of type direct).
53
+ # @api public
54
+ def self.default(channel_or_connection)
55
+ self.new(channel_or_connection, :direct, AMQ::Protocol::EMPTY_STRING, :no_declare => true)
56
+ end
57
+
58
+ # @param [Bunny::Channel] channel Channel this exchange will use.
59
+ # @param [Symbol,String] type Exchange type
60
+ # @param [String] name Exchange name
61
+ # @param [Hash] opts Exchange properties
62
+ #
63
+ # @option opts [Boolean] :durable (false) Should this exchange be durable?
64
+ # @option opts [Boolean] :auto_delete (false) Should this exchange be automatically deleted when it is no longer used?
65
+ # @option opts [Boolean] :arguments ({}) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
66
+ #
67
+ # @see Bunny::Channel#topic
68
+ # @see Bunny::Channel#fanout
69
+ # @see Bunny::Channel#direct
70
+ # @see Bunny::Channel#headers
71
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
72
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
73
+ # @api public
74
+ def initialize(channel, type, name, opts = {})
75
+ @channel = channel
76
+ @name = name
77
+ @type = type
78
+ @options = self.class.add_default_options(name, opts)
79
+
80
+ @durable = @options[:durable]
81
+ @auto_delete = @options[:auto_delete]
82
+ @internal = @options[:internal]
83
+ @arguments = @options[:arguments]
84
+
85
+ @bindings = Set.new
86
+
87
+ declare! unless opts[:no_declare] || predeclared? || (@name == AMQ::Protocol::EMPTY_STRING)
88
+
89
+ @channel.register_exchange(self)
90
+ end
91
+
92
+ # @return [Boolean] true if this exchange was declared as durable (will survive broker restart).
93
+ # @api public
94
+ def durable?
95
+ @durable
96
+ end # durable?
97
+
98
+ # @return [Boolean] true if this exchange was declared as automatically deleted (deleted as soon as last consumer unbinds).
99
+ # @api public
100
+ def auto_delete?
101
+ @auto_delete
102
+ end # auto_delete?
103
+
104
+ # @return [Boolean] true if this exchange is internal (used solely for exchange-to-exchange
105
+ # bindings and cannot be published to by clients)
106
+ def internal?
107
+ @internal
108
+ end
109
+
110
+ # @return [Hash] Additional optional arguments (typically used by RabbitMQ extensions and plugins)
111
+ # @api public
112
+ def arguments
113
+ @arguments
114
+ end
115
+
116
+
117
+ # Publishes a message
118
+ #
119
+ # @param [String] payload Message payload. It will never be modified by Bunny or RabbitMQ in any way.
120
+ # @param [Hash] opts Message properties (metadata) and delivery settings
121
+ #
122
+ # @option opts [String] :routing_key Routing key
123
+ # @option opts [Boolean] :persistent Should the message be persisted to disk?
124
+ # @option opts [Boolean] :mandatory Should the message be returned if it cannot be routed to any queue?
125
+ # @option opts [Integer] :timestamp A timestamp associated with this message
126
+ # @option opts [Integer] :expiration Expiration time after which the message will be deleted
127
+ # @option opts [String] :type Message type, e.g. what type of event or command this message represents. Can be any string
128
+ # @option opts [String] :reply_to Queue name other apps should send the response to
129
+ # @option opts [String] :content_type Message content type (e.g. application/json)
130
+ # @option opts [String] :content_encoding Message content encoding (e.g. gzip)
131
+ # @option opts [String] :correlation_id Message correlated to this one, e.g. what request this message is a reply for
132
+ # @option opts [Integer] :priority Message priority, 0 to 9. Not used by RabbitMQ, only applications
133
+ # @option opts [String] :message_id Any message identifier
134
+ # @option opts [String] :user_id Optional user ID. Verified by RabbitMQ against the actual connection username
135
+ # @option opts [String] :app_id Optional application ID
136
+ #
137
+ # @return [Bunny::Exchange] Self
138
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
139
+ # @api public
140
+ def publish(payload, opts = {})
141
+ @channel.basic_publish(payload, self.name, (opts.delete(:routing_key) || opts.delete(:key)), opts)
142
+
143
+ self
144
+ end
145
+
146
+
147
+ # Deletes the exchange unless it is predeclared
148
+ #
149
+ # @param [Hash] opts Options
150
+ #
151
+ # @option opts [Boolean] if_unused (false) Should this exchange be deleted only if it is no longer used
152
+ #
153
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
154
+ # @api public
155
+ def delete(opts = {})
156
+ @channel.deregister_exchange(self)
157
+ @channel.exchange_delete(@name, opts) unless predeclared?
158
+ end
159
+
160
+ # Binds an exchange to another (source) exchange using exchange.bind AMQP 0.9.1 extension
161
+ # that RabbitMQ provides.
162
+ #
163
+ # @param [String] source Source exchange name
164
+ # @param [Hash] opts Options
165
+ #
166
+ # @option opts [String] routing_key (nil) Routing key used for binding
167
+ # @option opts [Hash] arguments ({}) Optional arguments
168
+ #
169
+ # @return [Bunny::Exchange] Self
170
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
171
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
172
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
173
+ # @api public
174
+ def bind(source, opts = {})
175
+ @channel.exchange_bind(source, self, opts)
176
+ @bindings.add(source: source, opts: opts)
177
+
178
+ self
179
+ end
180
+
181
+ # Unbinds an exchange from another (source) exchange using exchange.unbind AMQP 0.9.1 extension
182
+ # that RabbitMQ provides.
183
+ #
184
+ # @param [String] source Source exchange name
185
+ # @param [Hash] opts Options
186
+ #
187
+ # @option opts [String] routing_key (nil) Routing key used for binding
188
+ # @option opts [Hash] arguments ({}) Optional arguments
189
+ #
190
+ # @return [Bunny::Exchange] Self
191
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
192
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
193
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
194
+ # @api public
195
+ def unbind(source, opts = {})
196
+ @channel.exchange_unbind(source, self, opts)
197
+ @bindings.delete(source: source, opts: opts)
198
+
199
+ self
200
+ end
201
+
202
+ # Defines a block that will handle returned messages
203
+ # @see http://rubybunny.info/articles/exchanges.html
204
+ # @api public
205
+ def on_return(&block)
206
+ @on_return = block
207
+
208
+ self
209
+ end
210
+
211
+ # Waits until all outstanding publisher confirms on the channel
212
+ # arrive.
213
+ #
214
+ # This is a convenience method that delegates to {Bunny::Channel#wait_for_confirms}
215
+ #
216
+ # @api public
217
+ def wait_for_confirms
218
+ @channel.wait_for_confirms
219
+ end
220
+
221
+ # @private
222
+ def recover_from_network_failure
223
+ declare! unless @options[:no_declare] ||predefined?
224
+
225
+ @bindings.each do |b|
226
+ bind(b[:source], b[:opts])
227
+ end
228
+ end
229
+
230
+
231
+ #
232
+ # Implementation
233
+ #
234
+
235
+ # @private
236
+ def handle_return(basic_return, properties, content)
237
+ if @on_return
238
+ @on_return.call(basic_return, properties, content)
239
+ else
240
+ # TODO: log a warning
241
+ end
242
+ end
243
+
244
+ # @return [Boolean] true if this exchange is a pre-defined one (amq.direct, amq.fanout, amq.match and so on)
245
+ def predefined?
246
+ (@name == AMQ::Protocol::EMPTY_STRING) || !!(@name =~ /^amq\.(direct|fanout|topic|headers|match)/i)
247
+ end # predefined?
248
+ alias predeclared? predefined?
249
+
250
+ protected
251
+
252
+ # @private
253
+ def declare!
254
+ @channel.exchange_declare(@name, @type, @options)
255
+ end
256
+
257
+ # @private
258
+ def self.add_default_options(name, opts)
259
+ # :nowait is always false for Bunny
260
+ h = { :queue => name, :nowait => false }.merge(opts)
261
+
262
+ if name.empty?
263
+ {
264
+ :passive => false,
265
+ :durable => false,
266
+ :auto_delete => false,
267
+ :internal => false,
268
+ :arguments => nil
269
+ }.merge(h)
270
+ else
271
+ h
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,56 @@
1
+ module Bunny
2
+ # @private
3
+ module Framing
4
+ ENCODINGS_SUPPORTED = defined? Encoding
5
+ HEADER_SLICE = (0..6).freeze
6
+ DATA_SLICE = (7..-1).freeze
7
+ PAYLOAD_SLICE = (0..-2).freeze
8
+
9
+ # @private
10
+ module String
11
+ class Frame < AMQ::Protocol::Frame
12
+ def self.decode(string)
13
+ header = string[HEADER_SLICE]
14
+ type, channel, size = self.decode_header(header)
15
+ data = string[DATA_SLICE]
16
+ payload = data[PAYLOAD_SLICE]
17
+ frame_end = data[-1, 1]
18
+
19
+ frame_end.force_encoding(AMQ::Protocol::Frame::FINAL_OCTET.encoding) if ENCODINGS_SUPPORTED
20
+
21
+ # 1) the size is miscalculated
22
+ if payload.bytesize != size
23
+ raise BadLengthError.new(size, payload.bytesize)
24
+ end
25
+
26
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
27
+ raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
28
+
29
+ self.new(type, payload, channel)
30
+ end
31
+ end
32
+ end # String
33
+
34
+
35
+ # @private
36
+ module IO
37
+ class Frame < AMQ::Protocol::Frame
38
+ def self.decode(io)
39
+ header = io.read(7)
40
+ type, channel, size = self.decode_header(header)
41
+ data = io.read_fully(size + 1)
42
+ payload, frame_end = data[PAYLOAD_SLICE], data[-1, 1]
43
+
44
+ # 1) the size is miscalculated
45
+ if payload.bytesize != size
46
+ raise BadLengthError.new(size, payload.bytesize)
47
+ end
48
+
49
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
50
+ raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
51
+ self.new(type, payload, channel)
52
+ end # self.from
53
+ end # Frame
54
+ end # IO
55
+ end # Framing
56
+ end # Bunny
@@ -0,0 +1,83 @@
1
+ require "bunny/versioned_delivery_tag"
2
+
3
+ module Bunny
4
+ # Wraps {AMQ::Protocol::Basic::GetOk} to
5
+ # provide access to the delivery properties as immutable hash as
6
+ # well as methods.
7
+ class GetResponse
8
+
9
+ #
10
+ # Behaviors
11
+ #
12
+
13
+ include Enumerable
14
+
15
+ #
16
+ # API
17
+ #
18
+
19
+ # @return [Bunny::Channel] Channel this basic.get-ok response is on
20
+ attr_reader :channel
21
+
22
+ # @private
23
+ def initialize(get_ok, channel)
24
+ @get_ok = get_ok
25
+ @hash = {
26
+ :delivery_tag => @get_ok.delivery_tag,
27
+ :redelivered => @get_ok.redelivered,
28
+ :exchange => @get_ok.exchange,
29
+ :routing_key => @get_ok.routing_key,
30
+ :channel => channel
31
+ }
32
+ @channel = channel
33
+ end
34
+
35
+ # Iterates over the delivery properties
36
+ # @see Enumerable#each
37
+ def each(*args, &block)
38
+ @hash.each(*args, &block)
39
+ end
40
+
41
+ # Accesses delivery properties by key
42
+ # @see Hash#[]
43
+ def [](k)
44
+ @hash[k]
45
+ end
46
+
47
+ # @return [Hash] Hash representation of this delivery info
48
+ def to_hash
49
+ @hash
50
+ end
51
+
52
+ # @private
53
+ def to_s
54
+ to_hash.to_s
55
+ end
56
+
57
+ # @private
58
+ def inspect
59
+ to_hash.inspect
60
+ end
61
+
62
+ # @return [String] Delivery identifier that is used to acknowledge, reject and nack deliveries
63
+ def delivery_tag
64
+ @get_ok.delivery_tag
65
+ end
66
+
67
+ # @return [Boolean] true if this delivery is a redelivery (the message was requeued at least once)
68
+ def redelivered
69
+ @get_ok.redelivered
70
+ end
71
+ alias redelivered? redelivered
72
+
73
+ # @return [String] Name of the exchange this message was published to
74
+ def exchange
75
+ @get_ok.exchange
76
+ end
77
+
78
+ # @return [String] Routing key this message was published with
79
+ def routing_key
80
+ @get_ok.routing_key
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,71 @@
1
+ require "thread"
2
+ require "amq/protocol/client"
3
+ require "amq/protocol/frame"
4
+
5
+ module Bunny
6
+ # Periodically sends heartbeats, keeping track of the last publishing activity.
7
+ #
8
+ # @private
9
+ class HeartbeatSender
10
+
11
+ #
12
+ # API
13
+ #
14
+
15
+ def initialize(transport, logger)
16
+ @transport = transport
17
+ @logger = logger
18
+ @mutex = Monitor.new
19
+
20
+ @last_activity_time = Time.now
21
+ end
22
+
23
+ def start(period = 30)
24
+ @mutex.synchronize do
25
+ # calculate interval as half the given period plus
26
+ # some compensation for Ruby's implementation inaccuracy
27
+ # (we cannot get at the nanos level the Java client uses, and
28
+ # our approach is simplistic). MK.
29
+ @interval = [(period / 2) - 1, 0.4].max
30
+
31
+ @thread = Thread.new(&method(:run))
32
+ @thread.report_on_exception = false if @thread.respond_to?(:report_on_exception)
33
+ end
34
+ end
35
+
36
+ def stop
37
+ @mutex.synchronize { @thread.exit }
38
+ end
39
+
40
+ def signal_activity!
41
+ @last_activity_time = Time.now
42
+ end
43
+
44
+ protected
45
+
46
+ def run
47
+ begin
48
+ loop do
49
+ self.beat
50
+
51
+ sleep @interval
52
+ end
53
+ rescue IOError => ioe
54
+ @logger.error "I/O error in the hearbeat sender: #{ioe.message}"
55
+ stop
56
+ rescue Exception => e
57
+ @logger.error "Error in the hearbeat sender: #{e.message}"
58
+ stop
59
+ end
60
+ end
61
+
62
+ def beat
63
+ now = Time.now
64
+
65
+ if now > (@last_activity_time + @interval)
66
+ @logger.debug { "Sending a heartbeat, last activity time: #{@last_activity_time}, interval (s): #{@interval}" }
67
+ @transport.write_without_timeout(AMQ::Protocol::HeartbeatFrame.encode, true)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ require "bunny/cruby/socket"
2
+
3
+ module Bunny
4
+ module JRuby
5
+ # TCP socket extension that uses Socket#readpartial to avoid excessive CPU
6
+ # burn after some time. See issue #165.
7
+ # @private
8
+ module Socket
9
+ include Bunny::Socket
10
+
11
+ def self.open(host, port, options = {})
12
+ socket = ::Socket.tcp(host, port, nil, nil,
13
+ connect_timeout: options[:connect_timeout])
14
+ if ::Socket.constants.include?('TCP_NODELAY') || ::Socket.constants.include?(:TCP_NODELAY)
15
+ socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
16
+ end
17
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options.fetch(:keepalive, true)
18
+ socket.extend self
19
+ socket.options = { :host => host, :port => port }.merge(options)
20
+ socket
21
+ rescue Errno::ETIMEDOUT
22
+ raise ClientTimeout
23
+ end
24
+
25
+ # Reads given number of bytes with an optional timeout
26
+ #
27
+ # @param [Integer] count How many bytes to read
28
+ # @param [Integer] timeout Timeout
29
+ #
30
+ # @return [String] Data read from the socket
31
+ # @api public
32
+ def read_fully(count, timeout = nil)
33
+ value = ''
34
+
35
+ begin
36
+ loop do
37
+ value << read_nonblock(count - value.bytesize)
38
+ break if value.bytesize >= count
39
+ end
40
+ rescue EOFError
41
+ # JRuby specific fix via https://github.com/jruby/jruby/issues/1694#issuecomment-54873532
42
+ IO.select([self], nil, nil, timeout)
43
+ retry
44
+ rescue *READ_RETRY_EXCEPTION_CLASSES
45
+ if IO.select([self], nil, nil, timeout)
46
+ retry
47
+ else
48
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
49
+ end
50
+ end
51
+
52
+ value
53
+ end # read_fully
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,58 @@
1
+ module Bunny
2
+ module JRuby
3
+ begin
4
+ require "bunny/cruby/ssl_socket"
5
+ require "openssl"
6
+
7
+ # TLS-enabled TCP socket that implements convenience
8
+ # methods found in Bunny::Socket.
9
+ class SSLSocket < Bunny::SSLSocket
10
+
11
+ def initialize(*args)
12
+ super
13
+ @__bunny_socket_eof_flag__ = false
14
+ end
15
+
16
+ # Reads given number of bytes with an optional timeout
17
+ #
18
+ # @param [Integer] count How many bytes to read
19
+ # @param [Integer] timeout Timeout
20
+ #
21
+ # @return [String] Data read from the socket
22
+ # @api public
23
+ def read_fully(count, timeout = nil)
24
+ return nil if @__bunny_socket_eof_flag__
25
+
26
+ value = ''
27
+ begin
28
+ loop do
29
+ value << read_nonblock(count - value.bytesize)
30
+ break if value.bytesize >= count
31
+ end
32
+ rescue EOFError => e
33
+ @__bunny_socket_eof_flag__ = true
34
+ rescue OpenSSL::SSL::SSLError => e
35
+ if e.message == "read would block"
36
+ if IO.select([self], nil, nil, timeout)
37
+ retry
38
+ else
39
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
40
+ end
41
+ else
42
+ raise e
43
+ end
44
+ rescue *READ_RETRY_EXCEPTION_CLASSES => e
45
+ if IO.select([self], nil, nil, timeout)
46
+ retry
47
+ else
48
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
49
+ end
50
+ end
51
+ value
52
+ end
53
+ end
54
+ rescue LoadError => le
55
+ puts "Could not load OpenSSL"
56
+ end
57
+ end
58
+ end