cinder RPC 通信源碼分析

cinder RPC 分析

[TOC]


我們都知道在Cinder內部,各組件之間通訊是通過RPC api,比如cinder-api創建卷,會通過RPC通知scheduler,然后scheduler再選擇一個cinder-volume服務,rpc通知創建卷。

Cinder 系統架構

AMQP通信協議

Openstack 組件內部的 RPC(Remote Producer Call)機制的實現是基于 AMQP(Advanced Message Queuing Protocol)作為通訊模型,從而滿足組件內部的松耦合性。AMQP 是用于異步消息通訊的消息中間件協議,AMQP 模型有四個重要的角色:

  • Exchange:根據 Routing key 轉發消息到對應的 Message Queue 中
  • Routing key:用于 Exchange 判斷哪些消息需要發送對應的 Message Queue
  • Publisher:消息發送者,將消息發送的 Exchange 并指明 Routing Key,以便 Message Queue 可以正確的收到消息
  • Consumer:消息接受者,從 Message Queue 獲取消息
AMQP 消息

消息發布者 Publisher 將 Message 發送給 Exchange 并且說明 Routing Key。Exchange 負責根據 Message 的 Routing Key 進行路由,將 Message 正確地轉發給相應的 Message Queue。監聽在 Message Queue 上的 Consumer 將會從 Queue 中讀取消息。

Routing Key 是 Exchange 轉發信息的依據,因此每個消息都有一個 Routing Key 表明可以接受消息的目的地址,而每個 Message Queue 都可以通過將自己想要接收的 Routing Key 告訴 Exchange 進行 binding,這樣 Exchange 就可以將消息正確地轉發給相應的 Message Queue。圖 2 就是 AMQP 消息模型。

AMQP 定義了三種類型的 Exchange,不同類型 Exchange 實現不同的 routing 算法:

  • Direct Exchange:Point-to-Point 消息模式,消息點對點的通信模式,Direct Exchange 根據 Routing Key 進行精確匹配,只有對應的 Message Queue 會接受到消息
  • Topic Exchange:Publish-Subscribe(Pub-sub)消息模式,Topic Exchange 根據 Routing Key 進行模糊匹配,只要符合模式匹配的 Message Queue 都會收到消息。支持Routing-key用或#的模式,進行綁定。匹配一個單詞,#匹配0個或者多個單詞。例如,binding key *.user.# 匹配routing key為 usd.user和eur.user.db,但是不匹配user.hello。
  • Fanout Exchange:廣播消息模式,Fanout Exchange 將消息轉發到所有綁定到它的 Message Queue,而不考慮routing key的值。

OpenStack 目前支持的基于 AMQP 模型的 RPC backend 有 RabbitMQ、QPid、ZeroMQ,對應的具體實現模塊在 lib\site-packages\oslo_messaging\ _drivers目錄下,impl_*.py 分別為對應的不同 backend 的實現。cinder默認使用RabbitMQ。

RabbitMQ 介紹

作為消息隊列(MQ),是一種應用程序對應用程序的通信方法。MQ是消費(consumer)-生產者(proceduer)模型的一個典型的代表,一端往消息隊列中不斷寫入消息,而另一端則可以讀取或者訂閱隊列中的消息。
MQ的用處是在項目中,將一些無需即時返回且耗時的操作提取出來,進行異步處理,而這種異步處理的方式大大的節省了服務器的請求響應時間,從而提高了系統的吞吐量。

常用指令

  • 創建用戶: rabbitmqctl add_user <username> <password>
[root@localhost ~]#  rabbitmqctl add_user wyue wyue
Creating user "wyue" ...
  • 查看所有用戶: rabbitmqctl list_users
[root@localhost ~]# rabbitmqctl list_users
Listing users ...
guest   [administrator]
wyue    []
stackrabbit     []
  • 關閉rabbitmq: rabbitmqctl stop_app

  • 還原: rabbitmqctl reset

  • 啟動rabbitmqctl start_app

  • 設置權限rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
    在創建用戶后,必須對用戶設置權限,否則連接會被拒絕!

  • 查看所有隊列信息: rabbitmqctl list_queues
    所列格式是 queue_name | queue_length ,

    圖片.png

  • 查看所有Exchange信息: rabbitmqctl list_exchanges
    所列格式是 exchange_name | exchange_type , exchange是以name作為唯一的,如果你之前定義了一個exchange(name='task',type='topic'),下次又定義exchange(name='task',type='direct')是會報錯的!!

    圖片.png

  • 查看所有consumer信息: rabbitmqctl list_consumers

    圖片.png

    只有程序對queue添加consumer后,用list_consumers指令才能看到這個consumer的信息,如果程序運行結束,consumer就不再對queue做監聽了,用list_consumers指令便不能再查到之前consumer的信息。

我們了解下常用指令就行。python中已經有專門的工具庫kombu,接下來我們看看它是什么玩意。

Kombu

Kombu是一個為Python寫的消息庫,目標是為AMQ協議提供一個傻瓜式的高層接口,讓Python中的消息傳遞變得盡可能簡單,并且也提供一些常見消息傳遞問題的解決方案。可參考《Kombu:Python的消息庫》

消息隊列的使用過程

  1. 客戶端連接到消息隊列服務器,打開一個channel。
  2. 客戶端聲明一個exchange,并設置相關屬性。
  3. 客戶端聲明一個queue,并設置相關屬性。
  4. 客戶端使用routing key,在exchange和queue之間建立好綁定關系。
  5. 客戶端投遞消息到exchange。
  6. exchange接收到消息后,就根據消息的key和已經設置的binding,進行消息路由,將消息投遞到一個或多個隊列里。

范例

我們寫個rabbitmq的通信,producer通過direct類型的exchange發送‘hello’到rabbitmq的隊列,然后consumer從隊列中取出。

注意

  • kombu最好升級版本到4.1.0,之前我使用4.0,運行的時候打開AMQP channel的時候報異常,提示WinError 10042
  • 如果連接rabbitmq新用戶,請確認用戶已經賦權,否則會被rabbitmq拒絕連接。

entity.py:

from kombu import Exchange, Queue
# 定義direct類型的exchange,另外還有topic/fanout兩種類型
task_exchange = Exchange('tasks', type='direct')
# 創建一個隊列,定義routing_key,并且跟exchange做綁定
task_queue = Queue('wy_test_queue', task_exchange, routing_key='wy_test1')

send.py

from kombu import Connection
from kombu.messaging import Producer
from entity import task_exchange
from kombu.transport.base import Message
# 創建連接
connection = Connection('amqp://wyue:wyue@172.24.3.200:5672//')
# 在連接里建立一個通道
with connection.channel() as channel:
    # 初始化消息
    message = Message(channel=channel, body='Hello')
    # 定義消息發布者,綁定exchange
    producer = Producer(channel, exchange=task_exchange)
    # 選用routing_key,發布消息
    producer.publish(message.body, routing_key='wy_test1')

運行send.py后,檢查rabbitmq隊列里wy_test_queue有一條信息。

[root@localhost ~]# rabbitmqctl list_queues|grep wy       
wy_test_queue   1

recv.py:

from kombu import Connection
from kombu.messaging import Consumer
from entity import task_queue

connection = Connection('amqp://wyue:wyue@172.24.3.200:5672//')

def process_media(body, message):
    """消息回調函數"""
    print body
    # 確認消息已經收到
    message.ack()

if __name__ == '__main__':

        with connection.channel() as channel:
            # 對隊列創建一個消息消費者
            consumer = Consumer(channel, task_queue)
            # 注冊回調
            consumer.register_callback(process_media)
            consumer.consume()
            # 一直循環,除非收到某個事件退出,比如socket超時
            while True:
                connection.drain_events()

運行send.py后,檢查rabbitmq隊列里wy_test_queue已經沒有信息。

而openstack,對底層MQ也做了封裝。openstack能夠支持多種MQ底層,其中也支持rabbitmq。

oslo_messaging

openstack 使用工具包來實現rpc調用。openstack社區把RPC相關的功能作為OpenStack的一個依賴庫。其實oslo.messaging庫就是把rabbitmq的python庫做了封裝,考慮到了編程友好、性能、可靠性、異常的捕獲等諸多因素。讓各個項目的開發者聚焦于業務代碼的編寫,而不用考慮消息如何發送和接收。我們上文討論的源碼,多屬于oslo_messaging。

Cinder RPC加載

在cinder-api啟動時候,代碼cinder.cmd.api.main里可以看到rpc.init(CONF)做RPC的加載。

cinder.rpc.init

def init(conf):
    global TRANSPORT, NOTIFICATION_TRANSPORT, NOTIFIER
    exmods = get_allowed_exmods()
    # 初始化RPC用的transport
    TRANSPORT = messaging.get_transport(conf,
                                        allowed_remote_exmods=exmods,
                                        aliases=TRANSPORT_ALIASES)
    # 初始化RPC notification用的transport
    NOTIFICATION_TRANSPORT = messaging.get_notification_transport(
        conf,
        allowed_remote_exmods=exmods,
        aliases=TRANSPORT_ALIASES)

    # get_notification_transport has loaded oslo_messaging_notifications config
    # group, so we can now check if notifications are actually enabled.
    if utils.notifications_enabled(conf):
        # 定義序列化工具
        json_serializer = messaging.JsonPayloadSerializer()
        serializer = RequestContextSerializer(json_serializer)
        NOTIFIER = messaging.Notifier(NOTIFICATION_TRANSPORT,
                                      serializer=serializer)
    else:
        NOTIFIER = utils.DO_NOTHING       

調用oslo_messaging的get_transport方法。get_transport是一個工廠方法,可根據配置文件里的transport_url生成不同后端的TRANSPORT對象。

oslo_messaging.transport.get_transport

def get_transport(conf, url=None, allowed_remote_exmods=None, aliases=None):
    """A factory method for Transport objects.

    This method will construct a Transport object from transport configuration
    gleaned from the user's configuration and, optionally, a transport URL.

    If a transport URL is supplied as a parameter, any transport configuration
    contained in it takes precedence. If no transport URL is supplied, but
    there is a transport URL supplied in the user's configuration then that
    URL will take the place of the URL parameter. In both cases, any
    configuration not supplied in the transport URL may be taken from
    individual configuration parameters in the user's configuration.

    An example transport URL might be::

        rabbit://me:passwd@host:5672/virtual_host

    and can either be passed as a string or a TransportURL object.

    :param conf: the user configuration
    :type conf: cfg.ConfigOpts
    :param url: a transport URL
    :type url: str or TransportURL
    :param allowed_remote_exmods: a list of modules which a client using this
                                  transport will deserialize remote exceptions
                                  from
    :type allowed_remote_exmods: list
    :param aliases: A map of transport alias to transport name
    :type aliases: dict
    """
    allowed_remote_exmods = allowed_remote_exmods or []
    #  導入'transport_url','rpc_backend','control_exchange'三個配置,配置說明見下文分析
    conf.register_opts(_transport_opts)
    # 把url(cinder.conf:transport_url = rabbit://stackrabbit:secret@172.24.3.200:5672/)轉換成oslo_messaging.transport.TransportURL對象
    if not isinstance(url, TransportURL):
        url = TransportURL.parse(conf, url, aliases)
        # url打印出: <TransportURL transport='rabbit', hosts=[<TransportHost hostname='172.24.2.218', port=5672, username='stackrabbit', password='secret'>]>
        
    kwargs = dict(default_exchange=conf.control_exchange,
                  allowed_remote_exmods=allowed_remote_exmods)

    try:
        # 在oslo.messaging.driver這個命名空間下,綁定rabbitmq的驅動
        mgr = driver.DriverManager('oslo.messaging.drivers',
                                   url.transport.split('+')[0],
                                   invoke_on_load=True,
                                   invoke_args=[conf, url],
                                   invoke_kwds=kwargs)
    except RuntimeError as ex:
        raise DriverLoadFailure(url.transport, ex)
    
    return Transport(mgr.driver)

transport 配置項列表

配置項 默認值 說明
transport_url A URL representing the messaging driver to use and its full configuration.
rpc_backend rabbit The messaging driver to use, defaults to rabbit. Other drivers include amqp and zmq. 已經被棄用,被transport_url取代
control_exchange openstack The default exchange under which topics are scoped. May be overridden by an exchange name specified in the transport_url option.

serializer = RequestContextSerializer(serializer) 是消息的序列化處理,把cinder消息轉換成可以在網絡中傳送的格式。

總結

  1. cinder在啟動cinder-api服務的時候,把RPC相關環境加載好
  2. 加載的內容主要是rpc的transport和serializer
  3. transport是根據配置項transport_url和control_exchange創建的,可看做cinder和rpc后端(如rabbitmq)之間的消息中轉站。
  4. serializer是序列化工具,用于rpc發送消息的序列化轉換。

Cinder RPC 接口

scheduler和volume都有定義自己的RPC接口,我們以scheduler為例。

scheduler目錄結構

rpcapi.py文件開放了RPC api接口,manager.py則是RPC 方法的具體業務實現。

cinder.scheduler.rpcapi.SchedulerAPI繼承自cinder.rpc.RPCAPI

每一個RPC api初始化的時候,都要定義target、serializer、client等,__init__定義在cinder.rpc.RPCAPI,其它子RPCAPI可直接繼承使用:

cinder.rpc.RPCAPI#init

    def __init__(self):
        target = messaging.Target(topic=self.TOPIC,
                                  version=self.RPC_API_VERSION)
        obj_version_cap = self.determine_obj_version_cap()
        serializer = base.CinderObjectSerializer(obj_version_cap)

        rpc_version_cap = self.determine_rpc_version_cap()
        self.client = get_client(target, version_cap=rpc_version_cap,
                                 serializer=serializer)

而一些特別的參數比如RPC_API_VERSION、TOPIC則由子RPCAPI自己定義:

cinder.scheduler.rpcapi.SchedulerAPI

    RPC_API_VERSION = '3.5'
    RPC_DEFAULT_VERSION = '3.0'
    # cinder/common/constants.py:21定義的   SCHEDULER_BINARY = "cinder-scheduler"
    TOPIC = constants.SCHEDULER_TOPIC
    BINARY = 'cinder-scheduler'

如果我們要定義rpc api,直接在cinder.scheduler.rpcapi.SchedulerAPI里添加即可。比如我們寫一個say_hello的rpc api 的 demo:

    def say_hello(self, ctxt):
        version = '3.0'
        cctxt = self.client.prepare(version=version)
        cctxt.cast(ctxt, 'say_hello')

self.client 來自于cinder.rpc.get_client

def get_client(target, version_cap=None, serializer=None):
    # assert斷言是聲明其布爾值必須為真的判定,如果發生異常就說明表達示為假。 
    # 這里判斷TRANSPORT如果為空就斷言異常退出,TRANSPORT已經在cinder-api服務啟動時加載好了,可見上文。
    assert TRANSPORT is not None
    serializer = RequestContextSerializer(serializer)
    # 返回了oslo_messaging.rpc.client.RPCClient對象
    return messaging.RPCClient(TRANSPORT,
                               target,
                               version_cap=version_cap,
                               serializer=serializer)

self.client.prepare(version=version) ,用于準備rpc環境的上下文,返回oslo_messaging.rpc.client._CallContext對象。
而oslo_messaging.rpc.client._CallContext繼承了oslo_messaging.rpc.client._BaseCallContext。_BaseCallContext有兩個重要的RPCClient方法,分別是call和cast。

oslo_messaging.rpc.client._BaseCallContext#cast

    def cast(self, ctxt, method, **kwargs):
        """Invoke a method and return immediately. See RPCClient.cast()."""
        # 對request消息做格式序列化
        msg = self._make_message(ctxt, method, kwargs)
        # 對rpc上下文做格式序列化
        msg_ctxt = self.serializer.serialize_context(ctxt)
        # 檢查target的版本是否正確
        self._check_version_cap(msg.get('version'))

        try:
            self.transport._send(self.target, msg_ctxt, msg, retry=self.retry)
        except driver_base.TransportDriverError as ex:
            raise ClientSendError(self.target, ex)

cast方法就是直接發出序列化的消息到target,不需要接收返回值。這屬于異步調用。

oslo_messaging.rpc.client._BaseCallContext#call

    def call(self, ctxt, method, **kwargs):
        """Invoke a method and wait for a reply. See RPCClient.call()."""
        if self.target.fanout:
            raise exceptions.InvalidTarget('A call cannot be used with fanout',
                                           self.target)

        msg = self._make_message(ctxt, method, kwargs)
        msg_ctxt = self.serializer.serialize_context(ctxt)

        timeout = self.timeout
        if self.timeout is None:
            timeout = self.conf.rpc_response_timeout

        self._check_version_cap(msg.get('version'))

        try:
            result = self.transport._send(self.target, msg_ctxt, msg,
                                          wait_for_reply=True, timeout=timeout,
                                          retry=self.retry)
        except driver_base.TransportDriverError as ex:
            raise ClientSendError(self.target, ex)

        return self.serializer.deserialize_entity(ctxt, result)

call發出了rpc消息,在timeout超時時間內,接收到響應信息并反序列化后返回。這是同步調用。

call和cast都是調用self.transport._send,我們來看看self.transport._send方法。

oslo_messaging.transport.Transport#_send

    def _send(self, target, ctxt, message, wait_for_reply=None, timeout=None,
              retry=None):
        if not target.topic:
            raise exceptions.InvalidTarget('A topic is required to send',
                                           target)
        return self._driver.send(target, ctxt, message,
                                 wait_for_reply=wait_for_reply,
                                 timeout=timeout, retry=retry)

而實際還是通過驅動的rabbitmq實現,即調用oslo_messaging._drivers.impl_rabbit.RabbitDriver的send方法,而這個方法繼承自oslo_messaging._drivers.amqpdriver.AMQPDriverBase的send方法,send又調用了_send。

oslo_messaging._drivers.amqpdriver.AMQPDriverBase#_send


    def _send(self, target, ctxt, message,
              wait_for_reply=None, timeout=None,
              envelope=True, notify=False, retry=None):

        msg = message
        # 如果wait_for_reply=True,等待回復。修改msg的數據結構。
        if wait_for_reply:
            # 生成msg_id。
            # uuid.uuid4由偽隨機數得到,轉換16進制。可見參考文檔《Python使用UUID庫生成唯一ID》
            msg_id = uuid.uuid4().hex
            msg.update({'_msg_id': msg_id})
            # _get_reply_q()設置回復信息的msgid,創建一個用于監聽回復消息的socket連接。具體見下文分析
            msg.update({'_reply_q': self._get_reply_q()})
        # msg結構體增加UNIQUE_ID,作為唯一性標識,避免重復msg
        rpc_amqp._add_unique_id(msg)
        unique_id = msg[rpc_amqp.UNIQUE_ID]
        # 把ctxt上下文整合進msg
        rpc_amqp.pack_context(msg, ctxt)
        # 對msg做序列化
        if envelope:
            msg = rpc_common.serialize_msg(msg)
       
        if wait_for_reply:
            # msg_id加入監聽隊列,用于接受返回值,msg_id就是監聽的key
            self._waiter.listen(msg_id)
            log_msg = "CALL msg_id: %s " % msg_id
        else:
            log_msg = "CAST unique_id: %s " % unique_id

        try:
            # 創建一個用于發送消息的socket連接
            with self._get_connection(rpc_common.PURPOSE_SEND) as conn:
                if notify:
                    exchange = self._get_exchange(target)
                    log_msg += "NOTIFY exchange '%(exchange)s'" \
                               " topic '%(topic)s'" % {
                                   'exchange': exchange,
                                   'topic': target.topic}
                    LOG.debug(log_msg)
                    conn.notify_send(exchange, target.topic, msg, retry=retry)
                elif target.fanout:
                    log_msg += "FANOUT topic '%(topic)s'" % {
                        'topic': target.topic}
                    LOG.debug(log_msg)
                    conn.fanout_send(target.topic, msg, retry=retry)
                else:
                    topic = target.topic
                    exchange = self._get_exchange(target)
                    if target.server:
                        topic = '%s.%s' % (target.topic, target.server)
                    # 例如:exchange 'openstack' topic 'cinder-scheduler'
                    log_msg += "exchange '%(exchange)s'" \
                               " topic '%(topic)s'" % {
                                   'exchange': exchange,
                                   'topic': topic}
                    LOG.debug(log_msg)
                    # 創建exchange,發送給publisher。
                    conn.topic_send(exchange_name=exchange, topic=topic,
                                    msg=msg, timeout=timeout, retry=retry)

            if wait_for_reply:
                # oslo_messaging._drivers.amqpdriver.ReplyWaiter#wait 等待rpc返回值
                result = self._waiter.wait(msg_id, timeout)
                # 如果返回值是個異常類型,則拋出
                if isinstance(result, Exception):
                    raise result
                return result
        finally:
            if wait_for_reply:
                self._waiter.unlisten(msg_id)
    def _get_reply_q(self):
        # 其實是with threading.Lock(),加線程鎖
        with self._reply_q_lock:
            if self._reply_q is not None:
                return self._reply_q
            
            reply_q = 'reply_' + uuid.uuid4().hex
            # 創建監聽模式的連接。oslo_messaging定義了兩種PURPOSE用于創建Connection,'listen'和 'send'。'listen'用于讀socket,'send'用于寫socket。
            conn = self._get_connection(rpc_common.PURPOSE_LISTEN)
            # 初始化回復監聽器
            self._waiter = ReplyWaiter(reply_q, conn,
                                       self._allowed_remote_exmods)

            self._reply_q = reply_q
            self._reply_q_conn = conn

        return self._reply_q

oslo_messaging._drivers.impl_rabbit.Connection#topic_send

    def topic_send(self, exchange_name, topic, msg, timeout=None, retry=None):
        """Send a 'topic' message."""
        # 創建kombu.entity.Exchange對象
        exchange = kombu.entity.Exchange(
            name=exchange_name,
            type='topic',
            durable=self.amqp_durable_queues,
            auto_delete=self.amqp_auto_delete)
        # 其實調用的是oslo_messaging._drivers.impl_rabbit.Connection#_publish,發布消息
        self._ensure_publishing(self._publish, exchange, msg,
                                routing_key=topic, timeout=timeout,
                                retry=retry)

oslo_messaging._drivers.impl_rabbit.Connection#_publish

    def _publish(self, exchange, msg, routing_key=None, timeout=None):
        """Publish a message."""
        # 檢查exchange有沒有在_declared_exchanges隊列,如果沒有,就加入。
        # _declared_exchanges適用于存儲exchanges的隊列,避免不必要的exchange重新定義。如果connection被重置了,Connection._set_current_channel也會對_declared_exchanges做重置。
        if not (exchange.passive or exchange.name in self._declared_exchanges):
                exchange(self.channel).declare()
                self._declared_exchanges.add(exchange.name)

        # NOTE(sileht): no need to wait more, caller expects
        # a answer before timeout is reached
        with self._transport_socket_timeout(timeout):
            # 調用kombu.messaging.Producer#publish,這里就不深入分析了。
            self._producer.publish(msg,
                                   exchange=exchange,
                                   routing_key=routing_key,
                                   expiration=timeout,
                                   compression=self.kombu_compression)

oslo_messaging里除了topic_send,還定義了direct_send、fanout_send兩種發送方法。三者對應了AMQP協議中Exchange的3種類型:Direct, Topic, Fanout。通過代碼看,它們的實現都是先定義一個exchange,然后通過oslo_messaging._drivers.impl_rabbit.Connection#_ensure_publishing方法,最后交給kombu.messaging.Producer#publish發布消息。

cinder中有用到的exchange:

exchange_name exchange_type
cinder-volume.localhost.localdomain@NetAppIscsiBackend_fanout fanout
cinder-volume.localhost.localdomain@ceph_fanout fanout
cinder-volume_fanout fanout
cinder-scheduler_fanout fanout
cinder-volume.localhost.localdomain@ceph-image_fanout fanout
openstack topic
    def direct_send(self, msg_id, msg):
        """Send a 'direct' message."""
        exchange = kombu.entity.Exchange(name=msg_id,
                                         type='direct',
                                         durable=False,
                                         auto_delete=True,
                                         passive=True)

        self._ensure_publishing(self._publish_and_raises_on_missing_exchange,
                                exchange, msg, routing_key=msg_id)

    def fanout_send(self, topic, msg, retry=None):
        """Send a 'fanout' message."""
        exchange = kombu.entity.Exchange(name='%s_fanout' % topic,
                                         type='fanout',
                                         durable=False,
                                         auto_delete=True)

        self._ensure_publishing(self._publish, exchange, msg, retry=retry)

oslo_messaging._drivers.amqpdriver.ReplyWaiter#wait:

    def wait(self, msg_id, timeout):
        # NOTE(sileht): for each msg_id we receive two amqp message
        # first one with the payload, a second one to ensure the other
        # have finish to send the payload
        # NOTE(viktors): We are going to remove this behavior in the N
        # release, but we need to keep backward compatibility, so we should
        # support both cases for now.
        timer = rpc_common.DecayingTimer(duration=timeout)
        timer.start()
        final_reply = None
        ending = False
        while not ending:
            timeout = timer.check_return(self._raise_timeout_exception, msg_id)
            try:
                message = self.waiters.get(msg_id, timeout=timeout)
            except moves.queue.Empty:
                self._raise_timeout_exception(msg_id)

            reply, ending = self._process_reply(message)
            if reply is not None:
                # NOTE(viktors): This can be either first _send_reply() with an
                # empty `result` field or a second _send_reply() with
                # ending=True and no `result` field.
                final_reply = reply
        return final_reply

其它

oslo.versionedobjects

The oslo.versionedobjects library provides a generic versioned object model that is RPC-friendly, with inbuilt serialization, field typing, and remotable method calls. It can be used to define a data model within a project independent of external APIs or database schema for the purposes of providing upgrade compatibility across distributed services.

參考博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容