Сетевые запросы, socket timeout и зависания тестов

Почти все, кто пишет автотесты, рано или поздно сталкивается с тем, что тест может зависнуть, по причине того что тестируемый сервер перестал отвечать. При этом у сокета, через который ведется общение, не установлен timeout. Так происходит, потому что разработчики либы для API-обращений как-то забывают про эту тонкость. Это настоящая головная боль, особенно когда тесты запущены на CI и неясно, что там происходит и когда закончится.

Выход безусловно есть. Н-р в python’e можно явно указать дефолтное значение socket timeout’a для всех создаваемых socket’ов:

import socket
socket.setdefaulttimeout(60)

И вроде бы все здорово, но вас будет ждать жестокое разочарование, если для запросов вы используете либу, работающую через мегапопулярный requests. Потому что там есть такой код: requests/connectionpool.py at 5524472cc76ea00d64181505f1fbb7f93f11cc2b · kennethreitz/requests · GitHub. В двух словах, там может установиться значение socket timeout в None (бесконечное время ожидания), переписав дефолтное заданное значение, т.е.:

import socket
socket.setdefaulttimeout(60)
sock = socket.socket()
sock.gettimeout()  # -> 60.0
sock.settimeout(None)
sock.gettimeout()  # -> None

Победить это можно, сделав monkeypatching для socket перед запуском тестов:

import socket


def settimeout():
    """Function to return wrapper over ``socket.socket.set_timeout``."""
    def wrapper(self, timeout):
        """Wrapper to prevent ability change default socket timeout to None.

        Note:
            This is workaround for https://github.com/kennethreitz/requests/blob/5524472cc76ea00d64181505f1fbb7f93f11cc2b/requests/packages/urllib3/connectionpool.py#L381  # noqa

        Args:
            timeout (int): Seconds of socket timeout.
        """
        if self.gettimeout() and timeout is None:
            return
        settimeout_func(self, timeout)

    settimeout_func = socket.socket.settimeout.im_func
    wrapper.__doc__ = settimeout_func.__doc__
    wrapper.__name__ = settimeout_func.__name__
    return wrapper

socket.socket.settimeout = settimeout()

После таких манипуляций, вызов settimeout(None) не будет перезаписывать установленное вами дефолтное значение, какую бы либу вы не использовали (вряд ли девелоперы либы тоже манкипатчат socket :)).

1 лайк