Skip to content
Merged
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
30 changes: 23 additions & 7 deletions graphene_linked_events/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import math
from types import SimpleNamespace

import requests
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import transaction
Expand Down Expand Up @@ -661,14 +662,29 @@ def resolve_popular_kultus_keywords(parent, info, **kwargs):
]
keywords = {}

# Graceful degradation: Continue processing remaining keyword sets
# even if one API request fails due to timeout or other errors.
# This prevents the entire query from failing when the LinkedEvents API
# is slow or experiencing issues.
for set_id in set_ids:
response = api_client.retrieve(
"keyword_set", set_id, params={"include": "keywords"}
)
response = json2obj(format_response(response))
for keyword in response.keywords:
if show_all_keywords or keyword.n_events > 0:
keywords[keyword.id] = keyword
try:
response = api_client.retrieve(
"keyword_set", set_id, params={"include": "keywords"}
)
response = json2obj(format_response(response))
for keyword in response.keywords:
if show_all_keywords or keyword.n_events > 0:
keywords[keyword.id] = keyword
except (
requests.exceptions.ConnectTimeout,
requests.exceptions.Timeout,
requests.exceptions.RequestException,
) as e:
# Log the error but continue processing other keyword sets
logger.warning(
f"Failed to fetch keyword set '{set_id}' from LinkedEvents API: {e}"
)
continue

unique_keywords = sorted(
keywords.values(), key=lambda x: x.n_events, reverse=True
Expand Down
111 changes: 111 additions & 0 deletions graphene_linked_events/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
EVENT_DATA,
EVENTS_DATA,
KEYWORD_SET_DATA,
POPULAR_KEYWORD_SET_DATA,
UPDATE_EVENT_DATA,
)
from graphene_linked_events.tests.utils import MockResponse
Expand Down Expand Up @@ -2130,6 +2131,116 @@ def test_get_popular_kultus_keywords(
snapshot.assert_match(executed)


def test_get_popular_kultus_keywords_with_timeout(api_client, monkeypatch, settings):
"""Test that popularKultusKeywords handles ConnectTimeout gracefully."""
from requests.exceptions import ConnectTimeout

def mock_retrieve_with_timeout(
self, resource, id, params=None, is_event_staff=False
):
# Simulate timeout for the first keyword set
if id == settings.KEYWORD_SET_ID_MAPPING["CATEGORY"]:
raise ConnectTimeout("Connection to api.hel.fi timed out")
# Return valid data for other keyword sets
return MockResponse(status_code=200, json_data=POPULAR_KEYWORD_SET_DATA)

monkeypatch.setattr(
LinkedEventsApiClient,
"retrieve",
mock_retrieve_with_timeout,
)

executed = api_client.execute(
POPULAR_KULTUS_KEYWORDS_QUERY,
variables={"amount": None, "showAllKeywords": True},
)

# Should not error out, should return partial results
assert "errors" not in executed
assert executed["data"]["popularKultusKeywords"] is not None
# Should still have keywords from the other two sets
assert executed["data"]["popularKultusKeywords"]["meta"]["count"] >= 0


def test_get_popular_kultus_keywords_continues_after_timeout(
api_client, monkeypatch, settings
):
"""Test that resolver continues processing after one keyword set times out."""
from requests.exceptions import ConnectTimeout

call_count = {"count": 0}
set_ids_called = []

def mock_retrieve_with_partial_timeout(
self, resource, id, params=None, is_event_staff=False
):
call_count["count"] += 1
set_ids_called.append(id)
# Timeout on ADDITIONAL_CRITERIA (middle set)
if id == settings.KEYWORD_SET_ID_MAPPING["ADDITIONAL_CRITERIA"]:
raise ConnectTimeout("Connection timeout")
return MockResponse(status_code=200, json_data=POPULAR_KEYWORD_SET_DATA)

monkeypatch.setattr(
LinkedEventsApiClient,
"retrieve",
mock_retrieve_with_partial_timeout,
)

executed = api_client.execute(
POPULAR_KULTUS_KEYWORDS_QUERY,
variables={"amount": None, "showAllKeywords": True},
)

# All 3 keyword sets should have been attempted
assert call_count["count"] == 3
assert len(set_ids_called) == 3
# Should not error
assert "errors" not in executed
# Should return results from the 2 successful requests
assert executed["data"]["popularKultusKeywords"]["meta"]["count"] > 0


def test_get_popular_kultus_keywords_partial_results(api_client, monkeypatch, settings):
"""Test partial results when some API requests fail."""
from requests.exceptions import Timeout

def mock_retrieve_with_mixed_results(
self, resource, id, params=None, is_event_staff=False
):
# Fail TARGET_GROUP request
if id == settings.KEYWORD_SET_ID_MAPPING["TARGET_GROUP"]:
raise Timeout("Request timeout")
# Return data with different keywords for other sets
keyword_data = {
**POPULAR_KEYWORD_SET_DATA,
"keywords": [
{
**POPULAR_KEYWORD_SET_DATA["keywords"][0],
"id": f"keyword:{id}",
}
],
}
return MockResponse(status_code=200, json_data=keyword_data)

monkeypatch.setattr(
LinkedEventsApiClient,
"retrieve",
mock_retrieve_with_mixed_results,
)

executed = api_client.execute(
POPULAR_KULTUS_KEYWORDS_QUERY,
variables={"amount": None, "showAllKeywords": True},
)

# Should return partial results from successful requests
assert "errors" not in executed
assert executed["data"]["popularKultusKeywords"] is not None
# Should have keywords from 2 out of 3 sets
assert executed["data"]["popularKultusKeywords"]["meta"]["count"] == 2


@pytest.mark.parametrize(
"status_code,error_cls",
[(400, ApiBadRequestError), (404, ObjectDoesNotExistError)],
Expand Down