Статьи

Дайте нам сейчас хвалить ResourceWarnings

[Источник]

К счастью, питоны не ядовиты.

Пару лет назад, когда я начал использовать Python 3, его новые ResourceWarnings привели меня в ярость, и я столкнулся с ними . Разработчик ядра Python Ник Коглан терпеливо поправил меня, и я написал продолжение «Mollified About ResourceWarnings» .

И теперь ResourceWarning спас мой тухус.

Несколько месяцев назад я исправлял ошибку в Motor, моем асинхронном драйвере для MongoDB. У мотора есть copy_databaseметод, который я обобщу следующим образом:

@gen.coroutine
def copy_database(self, source, target):
    pool, socket = None, None
    try:
        pool = self.get_pool()
        socket = pool.get_socket()
        # ... several operations with the socket ...
    finally:
        if pool and socket:
            pool.return_socket(socket)

Ошибка произошла, когда исходная база данных была защищена паролем. get_socketВызов не обеспечил его подтверждён , прежде чем он пытался скопировать базу данных. Я исправил ошибку так:

@gen.coroutine
def copy_database(self, source, target):
    pool, socket = None, None
    try:
        member = self.get_cluster_member()
        socket = self.get_authenticated_socket_from_member(member)
        # ... several operations with the socket ...
    finally:
        if pool and socket:
            pool.return_socket(socket)

Упс. Я исправил ошибку аутентификации, но ввел утечку сокета. Так poolкак теперь всегда None, код в finallyпредложении никогда не выполняется. В этом примере ошибка очевидна, но реальный метод имеет длину 60 строк — достаточно, чтобы я не видел несоответствия между его первой и последней строками.

Я беспечно выпустил ошибку в Motor 0.2.

Видимо, мои пользователи мало звонят copy_database, так как никто не сообщал об утечке сокета. Я не удивлен: Motor оптимизирован для веб-приложений с высокой степенью параллелизма, а не для административных сценариев, которые копируют базы данных. Если вы хотите скопировать базу данных, вы должны использовать обычный драйвер PyMongo. И так ошибка скрывалась в течение трех месяцев.

В эти выходные я разделил Motor на два модуля: модуль «core», который общается с MongoDB, и модуль «framework», который использует Tornado для асинхронного ввода-вывода. После того, как я разделил два аспекта Motor, я создал второй «каркасный» модуль, который использует новый Asyncio Framework Python 3.4 вместо Tornado. copy_databaseбыл одним из первых методов, которые я тестировал в новом Motor-on-asyncio. Это относительно сложно, поэтому я использовал его для тренировки нового кода.

copy_databaseработал с asyncio! Но я еще не был готов праздновать:

ResourceWarning: unclosed <socket.socket fd=9, laddr=('127.0.0.1', 54065), raddr=('127.0.0.1', 27017)>

Это проклятое ResourceWarning. Я провел небольшой бинарный поиск по своему тестовому коду, пока не нашел его: я не возвращал сокет copy_database. Исправление очевидно:

@gen.coroutine
def copy_database(self, source, target):
    member, socket = None, None
    try:
        member = self.get_cluster_member()
        socket = self.get_authenticated_socket_from_member(member)
        # ... several operations with the socket ...
    finally:
        if socket:
            member.pool.return_socket(socket)

Я выпустил это исправление сегодня в Motor 0.3.2 .

Извлеченный урок: я был глуп, когда сделал свой код «устойчивым» к неожиданным условиям. Предыдущий код вернул сокет if pool and socket. Но если socketне ноль, poolне должно быть, либо. Так что if socketодного должно быть достаточно. Этот более простой код, который обрабатывает только тот случай, который я ожидаю, возник бы немедленно, когда я представил ошибку. Неправильная надежность моего предыдущего кода скрывала мою ошибку в течение нескольких месяцев.

Еще один урок: я наконец-то понимаю ценность ResourceWarnings. Они вынуждают меня решить, когда дорогие объекты будут освобождены, и они предупреждают меня, если я испорчу это. Я проверяю свои процедуры тестирования, чтобы убедиться, что ResourceWarnings отображаются. В идеале ResourceWarning должен быть преобразован в исключение, которое приводит к сбою моих юнит-тестов. Вы знаете, как это сделать?