diff --git a/CHANGELOG b/CHANGELOG index f62400d..81779b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -42,6 +42,8 @@ - enh: use the API action `dcor_group_list_for_user` (ckanext-dcor_schemas 1.2.0) for listing the collections of a user, with a fallback for older servers (#108) + - enh: do not retry API requests when the server name cannot be + resolved (DCOR-Aid froze for minutes on Windows with a wrong server) - ref: convert `dcoraid.cli` module into a package; the upload task CLI now lives in `dcoraid.cli.task_upload_task` - ref: use `super()`, PEP 604 optional type hints, and builtin/ diff --git a/dcoraid/api/ckan_api.py b/dcoraid/api/ckan_api.py index 87a0248..8e9bf60 100644 --- a/dcoraid/api/ckan_api.py +++ b/dcoraid/api/ckan_api.py @@ -8,6 +8,7 @@ import random import traceback import urllib.parse +from urllib3.exceptions import MaxRetryError, NameResolutionError from urllib3.util.retry import Retry from dclab.external.packaging import parse as parse_version @@ -34,8 +35,27 @@ #: List of supported resource suffixes SERVER_RSUFFIX: dict[str, list[str]] = {} + +class RetryExceptNameResolution(Retry): + """Retry strategy that gives up immediately if a host cannot be resolved + + A failed name lookup is rarely transient (e.g. a typo in the server + name), and on Windows every failed lookup may take half a minute. + """ + def increment(self, method=None, url=None, response=None, error=None, + _pool=None, _stacktrace=None): + if isinstance(error, NameResolutionError): + raise MaxRetryError(_pool, url, error) from error + return super().increment(method=method, + url=url, + response=response, + error=error, + _pool=_pool, + _stacktrace=_stacktrace) + + # Configure retry strategy -retry_strategy = Retry( +retry_strategy = RetryExceptNameResolution( total=3, # Total retries backoff_factor=1.5, # {backoff factor} * (2 ** ({number prev. retries})) connect=3, # Handle connection errors (e.g. because the server is busy) diff --git a/tests/test_api_base.py b/tests/test_api_base.py index 17b4aa6..4218c16 100644 --- a/tests/test_api_base.py +++ b/tests/test_api_base.py @@ -1,7 +1,10 @@ +import socket import time +from unittest import mock import uuid import pytest +import requests from dcoraid.api import ckan_api @@ -77,3 +80,22 @@ def test_api_hostname(server, api_hostname): api = ckan_api.CKANAPI(server=server, check_ckan_version=False) assert api.hostname == api_hostname + + +def test_api_no_retry_on_name_resolution_error(): + """Do not retry requests when the host name cannot be resolved""" + host = f"{uuid.uuid4()}.does.not.exist.example.com" + lookups = [] + + def getaddrinfo(name, *args, **kwargs): + lookups.append(name) + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + + api = ckan_api.CKANAPI(server=host, + check_ckan_version=False, + caching=False) + with (mock.patch("socket.getaddrinfo", side_effect=getaddrinfo), + pytest.raises(requests.exceptions.ConnectionError, + match="Failed to resolve")): + api.get("status_show") + assert len(lookups) == 1