Как я могу чисто выйти из Pyro-демона по запросу клиента?

#python #sockets #pyro

#python #розетки #pyro

Вопрос:

Я пытаюсь использовать Pyro для управления подчиненной машиной. Я повторно синхронизирую необходимые файлы python, запускаю сервер Pyro, выполняю некоторые действия с помощью удаленного управления, а затем я хочу сообщить серверу Pyro о завершении работы.

У меня возникли проблемы с завершением работы демона Pryo. Он либо зависает во время Daemon.close() вызова, либо, если я закомментирую эту строку, он завершается без корректного завершения работы сокета, что приведет к socket.error: [Errno 98] Address already in use тому, что я перезапущу сервер слишком рано.

Я не думаю, что SO_REUSEADDR является правильным решением, поскольку нечистое завершение работы сокета по-прежнему приводит к зависанию сокета в состоянии TIME_WAIT, что потенциально может вызвать проблемы у некоторых клиентов. Я думаю, что лучшее решение — убедить демона Pyro правильно закрыть свой сокет.

Неправильно ли вызывать Daemon.завершение работы() изнутри самого демона?

Если я запускаю сервер, а затем нажимаю CTRL-C без каких-либо подключенных клиентов, у меня нет никаких проблем ( Address already in use ошибок нет). Это делает возможным чистое завершение работы в большинстве случаев (при условии, что в остальном клиент и сервер в здравом уме).

Пример: server.py

 import Pyro4

class TestAPI:
    def __init__(self, daemon):
        self.daemon = daemon
    def hello(self, msg):
        print 'client said {}'.format(msg)
        return 'hola'
    def shutdown(self):
        print 'shutting down...'
        self.daemon.shutdown()

if __name__ == '__main__':
    daemon = Pyro4.Daemon(port=9999)
    tapi = TestAPI(daemon)
    uri = daemon.register(tapi, objectId='TestAPI')
    daemon.requestLoop()
    print 'exited requestLoop'
    daemon.close() # this hangs
    print 'daemon closed'
 

Пример: client.py

 import Pyro4

if __name__ == '__main__':
        uri = 'PYRO:TestAPI@localhost:9999'
        remote = Pyro4.Proxy(uri)
        response = remote.hello('hello')
        print 'server said {}'.format(response)
        try:
            remote.shutdown()
        except Pyro4.errors.ConnectionClosedError:
            pass
        print 'client exiting'
 

Комментарии:

1. Привет, Эрик. У меня никогда не было Address already in use для сервера Pyro, но я все время получаю его для Name Server . Нажатие CTRL C на сервере имен имеет 50%-ную вероятность возникновения этой ошибки, если я снова запущу сервер имен в течение 30 секунд. У вас уже было такое раньше?

Ответ №1:

Я думаю, что это можно сделать без использования тайм-аута или условия цикла, если вы shutdown() вызовете демона shutdown . Согласно http://pythonhosted.org/Pyro4/servercode.html#cleaning-up:

Другой возможностью является вызов Pyro4.core .Демон.завершение работы () для запущенного объекта bdaemon. Это также выйдет из цикла запроса и позволит вашему коду аккуратно убираться после себя, а также будет работать на потоковом сервере без каких-либо других требований.

Следующее работает на Python3.4.2 в Windows. @Pyro4.oneway Декоратор для shutdown здесь не нужен, но в некоторых ситуациях он нужен.

server.py

 import Pyro4
# using Python3.4.2

@Pyro4.expose
class TestAPI:
    def __init__(self, daemon):
        self.daemon = daemon
    def hello(self, msg):
        print('client said {}'.format(msg))
        return 'hola'
    @Pyro4.oneway   # in case call returns much later than daemon.shutdown
    def shutdown(self):
        print('shutting down...')
        self.daemon.shutdown()

if __name__ == '__main__':
    daemon = Pyro4.Daemon(port=9999)
    tapi = TestAPI(daemon)
    uri = daemon.register(tapi, objectId='TestAPI')
    daemon.requestLoop()
    print('exited requestLoop')
    daemon.close()
    print('daemon closed')
 

client.py

 import Pyro4
# using Python3.4.2

if __name__ == '__main__':
    uri = 'PYRO:TestAPI@localhost:9999'
    remote = Pyro4.Proxy(uri)
    response = remote.hello('hello')
    print('server said {}'.format(response))
    remote.shutdown()
    remote._pyroRelease()
    print('client exiting')
 

Ответ №2:

Я думаю, что я близок к решению: комбинация использования loopCondition параметра to requestloop() и значения конфигурации COMMTIMEOUT .

server.py

 import Pyro4
Pyro4.config.COMMTIMEOUT = 1.0 # without this daemon.close() hangs

class TestAPI:
    def __init__(self, daemon):
        self.daemon = daemon
        self.running = True
    def hello(self, msg):
        print 'client said {}'.format(msg)
        return 'hola'
    def shutdown(self):
        print 'shutting down...'
        self.running = False

if __name__ == '__main__':
    daemon = Pyro4.Daemon(port=9999)
    tapi = TestAPI(daemon)
    uri = daemon.register(tapi, objectId='TestAPI')
    def checkshutdown():
        return tapi.running
    daemon.requestLoop(loopCondition=checkshutdown) # permits self-shutdown
    print 'exited requestLoop'
    daemon.close()
    print 'daemon closed'
 

К сожалению, есть одно условие, при котором он по-прежнему оставляет сокет в состоянии TIME_WAIT. Если клиент закрывает свой сокет после сервера, то следующая попытка запустить сервер возвращает ту же Address already in use ошибку.

Единственный способ, который я могу найти, чтобы обойти это, — увеличить время ожидания сервера (или перевести его в режим ожидания на несколько секунд перед вызовом daemon.close() ) и убедиться, что клиент всегда звонит _pyroRelease() сразу после завершения вызова:

client.py

 import Pyro4

if __name__ == '__main__':
        uri = 'PYRO:TestAPI@localhost:9999'
        remote = Pyro4.Proxy(uri)
        response = remote.hello('hello')
        print 'server said {}'.format(response)
        remote.shutdown()
        remote._pyroRelease()
        print 'client exiting'
 

Я полагаю, что этого достаточно, но, учитывая несправедливость планирования и задержки в сети, по-прежнему разочаровывает, что это условие гонки скрывается.

Комментарии:

1. В ходе тестирования я обнаружил, что слишком агрессивное использование COMMTIMEOUT приводит к ложным сбоям, поэтому мне пришлось отложить это до 5 секунд. Еще одна причина, по которой это решение кажется не совсем правильным.