Class: Bunny::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/bunny/session.rb

Overview

Represents AMQP 0.9.1 connection (to a RabbitMQ node).

Constant Summary

DEFAULT_HOST =

Default host used for connection

"127.0.0.1"
DEFAULT_VHOST =

Default virtual host used for connection

"/"
DEFAULT_USER =

Default username used for connection

"guest"
DEFAULT_PASSWORD =

Default password used for connection

"guest"
DEFAULT_HEARTBEAT =

Default heartbeat interval, the same value as RabbitMQ 3.0 uses.

:server
DEFAULT_CHANNEL_MAX =
CHANNEL_MAX_LIMIT
DEFAULT_CLIENT_PROPERTIES =

RabbitMQ client metadata

{
  :capabilities => {
    :publisher_confirms           => true,
    :consumer_cancel_notify       => true,
    :exchange_exchange_bindings   => true,
    :basic.nack"                 => true,
    :connection.blocked"         => true,
    # See http://www.rabbitmq.com/auth-notification.html
    :authentication_failure_close => true
  },
  :product      => "Bunny",
  :platform     => ::RUBY_DESCRIPTION,
  :version      => Bunny::VERSION,
  :information  => "http://rubybunny.info",
}
DEFAULT_NETWORK_RECOVERY_INTERVAL =

Default reconnection interval for TCP connection failures

5.0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_string_or_opts = ENV['RABBITMQ_URL'], optz = Hash.new) ⇒ Session

Returns a new instance of Session

Parameters:

  • connection_string_or_opts (String, Hash) (defaults to: ENV['RABBITMQ_URL'])

    Connection string or a hash of connection options

  • optz (Hash) (defaults to: Hash.new)

    Extra options not related to connection

Options Hash (connection_string_or_opts):

  • :host (String) — default: "127.0.0.1"

    Hostname or IP address to connect to

  • :hosts (Array<String>) — default: ["127.0.0.1"]

    list of hostname or IP addresses to select hostname from when connecting

  • :addresses (Array<String>) — default: ["127.0.0.1:5672"]

    list of addresses to select hostname and port from when connecting

  • :port (Integer) — default: 5672

    Port RabbitMQ listens on

  • :username (String) — default: "guest"

    Username

  • :password (String) — default: "guest"

    Password

  • :vhost (String) — default: "/"

    Virtual host to use

  • :heartbeat (Integer) — default: 600

    Heartbeat interval. 0 means no heartbeat.

  • :network_recovery_interval (Integer) — default: 4

    Recovery interval periodic network recovery will use. This includes initial pause after network failure.

  • :tls (Boolean) — default: false

    Should TLS/SSL be used?

  • :tls_cert (String) — default: nil

    Path to client TLS/SSL certificate file (.pem)

  • :tls_key (String) — default: nil

    Path to client TLS/SSL private key file (.pem)

  • :tls_ca_certificates (Array<String>)

    Array of paths to TLS/SSL CA files (.pem), by default detected from OpenSSL configuration

  • :verify_peer (String) — default: true

    Whether TLS peer verification should be performed

  • :tls_version (Keyword) — default: negotiated

    What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)

  • :continuation_timeout (Integer) — default: 15000

    Timeout for client operations that expect a response (e.g. Queue#get), in milliseconds.

  • :connection_timeout (Integer) — default: 5

    Timeout in seconds for connecting to the server.

  • :hosts_shuffle_strategy (Proc)

    A Proc that reorders a list of host strings, defaults to Array#shuffle

  • :logger (Logger)

    The logger. If missing, one is created using :log_file and :log_level.

  • :log_file (IO, String)

    The file or path to use when creating a logger. Defaults to STDOUT.

  • :logfile (IO, String)

    DEPRECATED: use :log_file instead. The file or path to use when creating a logger. Defaults to STDOUT.

  • :log_level (Integer)

    The log level to use when creating a logger. Defaults to LOGGER::WARN

  • :automatically_recover (Boolean) — default: true

    Should automatically recover from network failures?

  • :recovery_attempts (Integer) — default: nil

    Max number of recovery attempts, nil means forever, 0 means never

  • :recover_from_connection_close (Boolean) — default: true

    Recover from server-sent connection.close

Options Hash (optz):

  • :auth_mechanism (String) — default: "PLAIN"

    Authentication mechanism, PLAIN or EXTERNAL

  • :locale (String) — default: "PLAIN"

    Locale RabbitMQ should use

See Also:



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/bunny/session.rb', line 129

def initialize(connection_string_or_opts = ENV['RABBITMQ_URL'], optz = Hash.new)
  opts = case (connection_string_or_opts)
         when nil then
           Hash.new
         when String then
           self.class.parse_uri(connection_string_or_opts)
         when Hash then
           connection_string_or_opts
         end.merge(optz)

  @default_hosts_shuffle_strategy = Proc.new { |hosts| hosts.shuffle }

  @opts            = opts
  log_file         = opts[:log_file] || opts[:logfile] || STDOUT
  log_level        = opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN
  # we might need to log a warning about ill-formatted IPv6 address but
  # progname includes hostname, so init like this first
  @logger          = opts.fetch(:logger, init_default_logger_without_progname(log_file, log_level))

  @addresses       = self.addresses_from(opts)
  @address_index   = 0

  # re-init, see above
  @logger          = opts.fetch(:logger, init_default_logger(log_file, log_level))

  @user            = self.username_from(opts)
  @pass            = self.password_from(opts)
  @vhost           = self.vhost_from(opts)
  @threaded        = opts.fetch(:threaded, true)

  validate_connection_options(opts)

  # should automatic recovery from network failures be used?
  @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
                             true
                           else
                             opts[:automatically_recover] || opts[:automatic_recovery]
                           end
  @recovery_attempts     = opts[:recovery_attempts]
  @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
  @recover_from_connection_close = opts.fetch(:recover_from_connection_close, true)
  # in ms
  @continuation_timeout   = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)

  @status             = :not_connected
  @blocked            = false

  # these are negotiated with the broker during the connection tuning phase
  @client_frame_max   = opts.fetch(:frame_max, DEFAULT_FRAME_MAX)
  @client_channel_max = normalize_client_channel_max(opts.fetch(:channel_max, DEFAULT_CHANNEL_MAX))
  # will be-renegotiated during connection tuning steps. MK.
  @channel_max        = @client_channel_max
  @client_heartbeat   = self.heartbeat_from(opts)

  @client_properties   = DEFAULT_CLIENT_PROPERTIES.merge(opts.fetch(:properties, {}))
  @mechanism           = opts.fetch(:auth_mechanism, "PLAIN")
  @credentials_encoder = credentials_encoder_for(@mechanism)
  @locale              = @opts.fetch(:locale, DEFAULT_LOCALE)

  @mutex_impl          = @opts.fetch(:mutex_impl, Monitor)

  # mutex for the channel id => channel hash
  @channel_mutex       = @mutex_impl.new
  # transport operations/continuations mutex. A workaround for
  # the non-reentrant Ruby mutexes. MK.
  @transport_mutex     = @mutex_impl.new
  @status_mutex        = @mutex_impl.new
  @address_index_mutex = @mutex_impl.new

  @channels            = Hash.new

  @origin_thread       = Thread.current

  self.reset_continuations
  self.initialize_transport
end

Instance Attribute Details

#channel_id_allocatorObject (readonly)

Returns the value of attribute channel_id_allocator



83
84
85
# File 'lib/bunny/session.rb', line 83

def channel_id_allocator
  @channel_id_allocator
end

#channel_maxObject (readonly)

Returns the value of attribute channel_max



81
82
83
# File 'lib/bunny/session.rb', line 81

def channel_max
  @channel_max
end

#continuation_timeoutInteger (readonly)

Returns Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 15000.

Returns:

  • (Integer)

    Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 15000.



90
91
92
# File 'lib/bunny/session.rb', line 90

def continuation_timeout
  @continuation_timeout
end

#frame_maxObject (readonly)

Returns the value of attribute frame_max



81
82
83
# File 'lib/bunny/session.rb', line 81

def frame_max
  @frame_max
end

#heartbeatObject (readonly)

Returns the value of attribute heartbeat



81
82
83
# File 'lib/bunny/session.rb', line 81

def heartbeat
  @heartbeat
end

#loggerLogger (readonly)

Returns:

  • (Logger)


88
89
90
# File 'lib/bunny/session.rb', line 88

def logger
  @logger
end

#mechanismString (readonly)

Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"

Returns:

  • (String)


86
87
88
# File 'lib/bunny/session.rb', line 86

def mechanism
  @mechanism
end

#network_recovery_intervalObject (readonly)

Returns the value of attribute network_recovery_interval



91
92
93
# File 'lib/bunny/session.rb', line 91

def network_recovery_interval
  @network_recovery_interval
end

#passObject (readonly)

Returns the value of attribute pass



81
82
83
# File 'lib/bunny/session.rb', line 81

def pass
  @pass
end

#portObject (readonly)

Returns the value of attribute port



81
82
83
# File 'lib/bunny/session.rb', line 81

def port
  @port
end

#server_authentication_mechanismsObject (readonly)

Returns the value of attribute server_authentication_mechanisms



82
83
84
# File 'lib/bunny/session.rb', line 82

def server_authentication_mechanisms
  @server_authentication_mechanisms
end

#server_capabilitiesObject (readonly)

Returns the value of attribute server_capabilities



82
83
84
# File 'lib/bunny/session.rb', line 82

def server_capabilities
  @server_capabilities
end

#server_localesObject (readonly)

Returns the value of attribute server_locales



82
83
84
# File 'lib/bunny/session.rb', line 82

def server_locales
  @server_locales
end

#server_propertiesObject (readonly)

Returns the value of attribute server_properties



82
83
84
# File 'lib/bunny/session.rb', line 82

def server_properties
  @server_properties
end

#statusObject (readonly)

Returns the value of attribute status



81
82
83
# File 'lib/bunny/session.rb', line 81

def status
  @status
end

#threadedObject (readonly)

Returns the value of attribute threaded



81
82
83
# File 'lib/bunny/session.rb', line 81

def threaded
  @threaded
end

#transportBunny::Transport (readonly)

Returns:

  • (Bunny::Transport)


80
81
82
# File 'lib/bunny/session.rb', line 80

def transport
  @transport
end

#userObject (readonly)

Returns the value of attribute user



81
82
83
# File 'lib/bunny/session.rb', line 81

def user
  @user
end

#vhostObject (readonly)

Returns the value of attribute vhost



81
82
83
# File 'lib/bunny/session.rb', line 81

def vhost
  @vhost
end

Class Method Details

.parse_uri(uri) ⇒ Hash

Parses an amqp[s] URI into a hash that #initialize accepts.

Parameters:

  • uri (String)

    amqp or amqps URI to parse

Returns:

  • (Hash)

    Parsed URI as a hash



448
449
450
# File 'lib/bunny/session.rb', line 448

def self.parse_uri(uri)
  AMQ::Settings.parse_amqp_url(uri)
end

Instance Method Details

#automatically_recover?Boolean

Returns true if this connection has automatic recovery from network failure enabled

Returns:

  • (Boolean)

    true if this connection has automatic recovery from network failure enabled



412
413
414
# File 'lib/bunny/session.rb', line 412

def automatically_recover?
  @automatically_recover
end

#blocked?Boolean

Returns true if the connection is currently blocked by RabbitMQ because it's running low on RAM, disk space, or other resource; false otherwise

Returns:

  • (Boolean)

    true if the connection is currently blocked by RabbitMQ because it's running low on RAM, disk space, or other resource; false otherwise

See Also:



440
441
442
# File 'lib/bunny/session.rb', line 440

def blocked?
  @blocked
end

#clean_up_and_fail_on_connection_close!(method) ⇒ Object



740
741
742
743
744
745
746
747
748
749
750
# File 'lib/bunny/session.rb', line 740

def clean_up_and_fail_on_connection_close!(method)
  @last_connection_error = instantiate_connection_level_exception(method)
  @continuations.push(method)

  clean_up_on_shutdown
  if threaded?
    @origin_thread.raise(@last_connection_error)
  else
    raise @last_connection_error
  end
end

#clean_up_on_shutdownObject



752
753
754
755
756
757
758
759
760
761
762
763
764
# File 'lib/bunny/session.rb', line 752

def clean_up_on_shutdown
  begin
    shut_down_all_consumer_work_pools!
    maybe_shutdown_reader_loop
    maybe_shutdown_heartbeat_sender
  rescue ShutdownSignal => sse
    # no-op
  rescue Exception => e
    @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
  ensure
    close_transport
  end
end

#closeObject Also known as: stop

Closes the connection. This involves closing all of its channels.



356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/bunny/session.rb', line 356

def close
  @status_mutex.synchronize { @status = :closing }

  ignoring_io_errors do
    if @transport.open?
      close_all_channels

      self.close_connection(true)
    end

    clean_up_on_shutdown
  end
  @status_mutex.synchronize { @status = :closed }
end

#closed?Boolean

Returns true if this AMQP 0.9.1 connection is closed

Returns:

  • (Boolean)

    true if this AMQP 0.9.1 connection is closed



399
400
401
# File 'lib/bunny/session.rb', line 399

def closed?
  @status_mutex.synchronize { @status == :closed }
end

#closing?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns true if this AMQP 0.9.1 connection is closing

Returns:

  • (Boolean)

    true if this AMQP 0.9.1 connection is closing



394
395
396
# File 'lib/bunny/session.rb', line 394

def closing?
  @status_mutex.synchronize { @status == :closing }
end

#configure_socket(&block) ⇒ Object

Provides a way to fine tune the socket used by connection. Accepts a block that the socket will be yielded to.

Raises:

  • (ArgumentError)


262
263
264
265
266
# File 'lib/bunny/session.rb', line 262

def configure_socket(&block)
  raise ArgumentError, "No block provided!" if block.nil?

  @transport.configure_socket(&block)
end

#connecting?Boolean

Returns true if this connection is still not fully open

Returns:

  • (Boolean)

    true if this connection is still not fully open



388
389
390
# File 'lib/bunny/session.rb', line 388

def connecting?
  status == :connecting
end

#create_channel(n = nil, consumer_pool_size = 1, consumer_pool_abort_on_exception = false, consumer_pool_shutdown_timeout = 60) ⇒ Bunny::Channel Also known as: channel

Opens a new channel and returns it. This method will block the calling thread until the response is received and the channel is guaranteed to be opened (this operation is very fast and inexpensive).

Returns:

Raises:

  • (ArgumentError)


340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/bunny/session.rb', line 340

def create_channel(n = nil, consumer_pool_size = 1, consumer_pool_abort_on_exception = false, consumer_pool_shutdown_timeout = 60)
  raise ArgumentError, "channel number 0 is reserved in the protocol and cannot be used" if 0 == n

  @channel_mutex.synchronize do
    if n && (ch = @channels[n])
      ch
    else
      ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1, consumer_pool_abort_on_exception, consumer_pool_shutdown_timeout))
      ch.open
      ch
    end
  end
end

#exchange_exists?(name) ⇒ Boolean

Checks if a exchange with given name exists.

Implemented using exchange.declare with passive set to true and a one-off (short lived) channel under the hood.

Parameters:

  • name (String)

    Exchange name

Returns:

  • (Boolean)

    true if exchange exists



480
481
482
483
484
485
486
487
488
489
490
# File 'lib/bunny/session.rb', line 480

def exchange_exists?(name)
  ch = create_channel
  begin
    ch.exchange(name, :passive => true)
    true
  rescue Bunny::NotFound => _
    false
  ensure
    ch.close if ch.open?
  end
end

#heartbeat_disabled?(val) ⇒ Boolean (protected)

Returns:

  • (Boolean)


1167
1168
1169
# File 'lib/bunny/session.rb', line 1167

def heartbeat_disabled?(val)
  0 == val || val.nil?
end

#heartbeat_intervalInteger

Returns Heartbeat interval used

Returns:

  • (Integer)

    Heartbeat interval used



226
# File 'lib/bunny/session.rb', line 226

def heartbeat_interval; self.heartbeat; end

#hostObject



245
246
247
# File 'lib/bunny/session.rb', line 245

def host
  @transport ? @transport.host : host_from_address(@addresses[@address_index])
end

#hostnameString

Returns RabbitMQ hostname (or IP address) used

Returns:

  • (String)

    RabbitMQ hostname (or IP address) used



217
# File 'lib/bunny/session.rb', line 217

def hostname;     self.host;  end

#ignoring_io_errors(&block) ⇒ Object (protected)



1309
1310
1311
1312
1313
1314
1315
# File 'lib/bunny/session.rb', line 1309

def ignoring_io_errors(&block)
  begin
    block.call
  rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Bunny::NetworkFailure => _
    # ignore
  end
end

#inspectObject



1037
1038
1039
# File 'lib/bunny/session.rb', line 1037

def inspect
  to_s
end

#local_portInteger

Returns Client socket port

Returns:

  • (Integer)

    Client socket port



269
270
271
# File 'lib/bunny/session.rb', line 269

def local_port
  @transport.local_address.ip_port
end

#normalize_client_channel_max(n) ⇒ Object (protected)



1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
# File 'lib/bunny/session.rb', line 1298

def normalize_client_channel_max(n)
  return CHANNEL_MAX_LIMIT if n > CHANNEL_MAX_LIMIT

  case n
  when 0 then
    CHANNEL_MAX_LIMIT
  else
    n
  end
end

#on_blocked {|AMQ::Protocol::Connection::Blocked| ... } ⇒ Object

Defines a callback that will be executed when RabbitMQ blocks the connection because it is running low on memory or disk space (as configured via config file and/or rabbitmqctl).

Yields:

  • (AMQ::Protocol::Connection::Blocked)

    connection.blocked method which provides a reason for blocking



423
424
425
# File 'lib/bunny/session.rb', line 423

def on_blocked(&block)
  @block_callback = block
end

#on_unblocked(&block) ⇒ Object

Defines a callback that will be executed when RabbitMQ unblocks the connection that was previously blocked, e.g. because the memory or disk space alarm has cleared.

See Also:



432
433
434
# File 'lib/bunny/session.rb', line 432

def on_unblocked(&block)
  @unblock_callback = block
end

#open?Boolean Also known as: connected?

Returns true if this AMQP 0.9.1 connection is open

Returns:

  • (Boolean)

    true if this AMQP 0.9.1 connection is open



404
405
406
407
408
# File 'lib/bunny/session.rb', line 404

def open?
  @status_mutex.synchronize do
    (status == :open || status == :connected || status == :connecting) && @transport.open?
  end
end

#passwordString

Returns Password used

Returns:

  • (String)

    Password used



221
# File 'lib/bunny/session.rb', line 221

def password;     self.pass;  end

#queue_exists?(name) ⇒ Boolean

Checks if a queue with given name exists.

Implemented using queue.declare with passive set to true and a one-off (short lived) channel under the hood.

Parameters:

  • name (String)

    Queue name

Returns:

  • (Boolean)

    true if queue exists



460
461
462
463
464
465
466
467
468
469
470
# File 'lib/bunny/session.rb', line 460

def queue_exists?(name)
  ch = create_channel
  begin
    ch.queue(name, :passive => true)
    true
  rescue Bunny::NotFound => _
    false
  ensure
    ch.close if ch.open?
  end
end

#reset_address_indexObject



253
254
255
# File 'lib/bunny/session.rb', line 253

def reset_address_index
  @address_index_mutex.synchronize { @address_index = 0 }
end

#startObject

Starts the connection process.



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/bunny/session.rb', line 278

def start

  return self if connected?

  @status_mutex.synchronize { @status = :connecting }
  # reset here for cases when automatic network recovery kicks in
  # when we were blocked. MK.
  @blocked       = false
  self.reset_continuations

  begin

    begin

      # close existing transport if we have one,
      # to not leak sockets
      @transport.maybe_initialize_socket

      @transport.post_initialize_socket
      @transport.connect

      if @socket_configurator
        @transport.configure_socket(&@socket_configurator)
      end

      self.init_connection
      self.open_connection

      @reader_loop = nil
      self.start_reader_loop if threaded?

    rescue TCPConnectionFailed => e
      @logger.warn e.message
      self.initialize_transport
      @logger.warn "Will try to connect to the next endpoint in line: #{@transport.host}:#{@transport.port}"

      return self.start
    rescue
      @status_mutex.synchronize { @status = :not_connected }
      raise
    end
  rescue HostListDepleted
    self.reset_address_index
    @status_mutex.synchronize { @status = :not_connected }
    raise TCPConnectionFailedForAllHosts
  end

  self
end

#threaded?Boolean

Returns true if this connection uses a separate thread for I/O activity

Returns:

  • (Boolean)

    true if this connection uses a separate thread for I/O activity



241
242
243
# File 'lib/bunny/session.rb', line 241

def threaded?
  @threaded
end

#to_sString

Returns:

  • (String)


1032
1033
1034
1035
# File 'lib/bunny/session.rb', line 1032

def to_s
  oid = ("0x%x" % (self.object_id << 1))
  "#<#{self.class.name}:#{oid} #{@user}@#{host}:#{port}, vhost=#{@vhost}, addresses=[#{@addresses.join(',')}]>"
end

#usernameString

Returns Username used

Returns:

  • (String)

    Username used



219
# File 'lib/bunny/session.rb', line 219

def username;     self.user;  end

#uses_ssl?Boolean Also known as: ssl?

Returns true if this connection uses TLS (SSL)

Returns:

  • (Boolean)

    true if this connection uses TLS (SSL)



235
236
237
# File 'lib/bunny/session.rb', line 235

def uses_ssl?
  @transport.uses_ssl?
end

#uses_tls?Boolean Also known as: tls?

Returns true if this connection uses TLS (SSL)

Returns:

  • (Boolean)

    true if this connection uses TLS (SSL)



229
230
231
# File 'lib/bunny/session.rb', line 229

def uses_tls?
  @transport.uses_tls?
end

#validate_connection_options(options) ⇒ Object



206
207
208
209
210
211
212
213
214
# File 'lib/bunny/session.rb', line 206

def validate_connection_options(options)
  if options[:hosts] && options[:addresses]
    raise ArgumentError, "Connection options can't contain hosts and addresses at the same time"
  end

  if (options[:host] || options[:hostname]) && (options[:hosts] || options[:addresses])
    @logger.warn "The connection options contain both a host and an array of hosts, the single host is ignored."
  end
end

#virtual_hostString

Returns Virtual host used

Returns:

  • (String)

    Virtual host used



223
# File 'lib/bunny/session.rb', line 223

def virtual_host; self.vhost; end

#with_channel(n = nil) ⇒ Bunny::Session

Creates a temporary channel, yields it to the block given to this method and closes it.

Returns:



376
377
378
379
380
381
382
383
384
385
# File 'lib/bunny/session.rb', line 376

def with_channel(n = nil)
  ch = create_channel(n)
  begin
    yield ch
  ensure
    ch.close if ch.open?
  end

  self
end