Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions integreat_cms/cms/views/utils/hix.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ def lookup_hix_score_helper(text: str) -> TextlabResult:
settings.TEXTLAB_API_USERNAME,
settings.TEXTLAB_API_KEY,
).benchmark(normalized_text)
except TimeoutError as e:
logger.warning(
"HIX benchmark API call timed out after %s seconds: %r",
settings.TEXTLAB_API_TIMEOUT,
e,
)
raise CacheMeIfYouCan from e
except (URLError, OSError) as e:
logger.warning("HIX benchmark API call failed: %r", e)
raise CacheMeIfYouCan from e
Expand Down
6 changes: 6 additions & 0 deletions integreat_cms/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@
420,
)

#: How many seconds to wait for the Textlab API to respond before giving up
TEXTLAB_API_TIMEOUT: Final[float] = env_float(
"INTEGREAT_CMS_TEXTLAB_API_TIMEOUT",
30,
)

#: The minimum HIX score required for machine translation
HIX_REQUIRED_FOR_MT: Final[float] = env_float(
"INTEGREAT_CMS_HIX_REQUIRED_FOR_MT",
Expand Down
2 changes: 1 addition & 1 deletion integreat_cms/textlab_api/textlab_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,5 @@ def post_request(
request.add_header("authorization", f"Bearer {auth_token}")
request.add_header("Content-Type", "application/json")
request.add_header("User-Agent", "")
with urlopen(request) as response: # noqa: S310
with urlopen(request, timeout=settings.TEXTLAB_API_TIMEOUT) as response: # noqa: S310
return json.loads(response.read().decode("utf-8"))
41 changes: 41 additions & 0 deletions tests/cms/views/utils/test_hix.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import math
import socket
import threading
import time
from copy import deepcopy
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -349,3 +352,41 @@ def test_contact_card_gets_filtered_out() -> None:

assert result is not None
assert result["score"] is None


def test_hix_lookup_times_out_instead_of_hanging(
settings: SettingsWrapper,
) -> None:
"""
If the Textlab API accepts the connection but never responds, ``lookup_hix_score()``
must give up after :attr:`~integreat_cms.core.settings.TEXTLAB_API_TIMEOUT` instead
of blocking forever (see #<issue-number>).
"""
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
port = listener.getsockname()[1]

def black_hole() -> None:
with listener:
conn, _ = listener.accept()
with conn:
# Deliberately never send a response, simulating a stalled API
time.sleep(2)

threading.Thread(target=black_hole, daemon=True).start()

settings.TEXTLAB_API_ENABLED = True
settings.TEXTLAB_API_URL = f"http://127.0.0.1:{port}"
settings.TEXTLAB_API_TIMEOUT = 0.2

start = time.monotonic()
result = lookup_hix_score(
"Unique text for test_hix_lookup_times_out_instead_of_hanging",
)
elapsed = time.monotonic() - start

assert result is None
assert elapsed < 1, (
"lookup_hix_score() should give up after TEXTLAB_API_TIMEOUT, not hang"
)
Loading