К счастью, питоны не ядовиты.
Пару лет назад, когда я начал использовать 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 должен быть преобразован в исключение, которое приводит к сбою моих юнит-тестов. Вы знаете, как это сделать?