docs: def publish(self, exchange, routing_key, body, properties=None):
          """Publish a message to RabbitMQ. If the RabbitMQ connection is not
          established or is blocked, attempt to wait until sending is possible.
  
          :param str exchange: The exchange to publish the message to.
          :param str routing_key: The routing key to publish the message with.
          :param bytes body: The message body to send.
          :param dict properties: An optional dict of additional properties
                                  to append.
          :rtype: tornado.concurrent.Future
          :raises: :exc:`sprockets.mixins.amqp.NotReadyError`
          :raises: :exc:`sprockets.mixins.amqp.PublishingError`
  
          """
          future = concurrent.Future()
  
          properties = properties or {}
          properties.setdefault('app_id', self.default_app_id)
          properties.setdefault('message_id', str(uuid.uuid4()))
          properties.setdefault('timestamp', int(time.time()))
  
          if self.ready:
              if self.publisher_confirmations:
                  self.message_number += 1
                  self.messages[self.message_number] = future
              else:
                  future.set_result(None)
  
              try:
                  self.channel.basic_publish(
                      exchange, routing_key, body,
                      pika.BasicProperties(**properties), True)
              except exceptions.AMQPError as error:
                  future.set_exception(
                      PublishingFailure(
                          properties['message_id'],
                          exchange, routing_key,
                          error.__class__.__name__))
          else:
              future.set_exception(NotReadyError(
                  self.state_description, properties['message_id']))
          return future