Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions main/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class Config:
COMMUNITY_DASHBOARD_DOMAIN = typing.cast("URLParseResult", settings.COMMUNITY_DASHBOARD_DOMAIN)
WEBSITE_DOMAIN = typing.cast("URLParseResult", settings.WEBSITE_DOMAIN)

# NOTE: Gunicorn worker timeout (misc/prod/run_web.sh) is 40s, keep external API
# timeouts a few seconds below that so requests fail with a clear error instead
# of the gunicorn worker being killed mid-request.
DEFAULT_EXTERNAL_API_TIMEOUT = 38

# NOTE: We get AOI for validate from HOT tasking manager
HOT_TASKING_MANAGER_PROJECT_API_LINK = "https://tasking-manager-production-api.hotosm.org/api/v2/"

Expand Down
32 changes: 25 additions & 7 deletions project_types/validate/api_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class ValidateApiCallError(Exception):
pass


class ValidateApiCallTimeoutError(ValidateApiCallError):
pass


def remove_troublesome_chars(string: str | None):
"""Remove chars that cause trouble when pushed into postgres."""
if type(string) is not str:
Expand Down Expand Up @@ -176,16 +180,22 @@ def remove_noise_and_add_user_info(json: dict[str, Any]) -> dict[str, Any]:
return json


# fixme(frozenhelium): merge this function with `ohsome` and also add appropriate messages to raised exceptions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this comment?

def get_object_count_from_ohsome(area: str, ohsome_filter: PydanticLongText) -> int | None:
def get_object_count_from_ohsome(
area: str,
ohsome_filter: PydanticLongText,
timeout: int = Config.DEFAULT_EXTERNAL_API_TIMEOUT,
) -> int | None:
url = Config.OHSOME_API_LINK + "elements/count"
data = {"bpolys": area, "filter": ohsome_filter}

logger.info("Target: %s", url)
logger.info("Filter: %s", ohsome_filter)

# fixme(frozenhelium): use httpx for proper timeout

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this comment? the default request timeout is not reliable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should i migrate it to httpx?

response = requests.post(url, data=data, timeout=100)
try:
response = requests.post(url, data=data, timeout=timeout)
except requests.exceptions.Timeout as e:
logger.warning("ohsome element count request timed out")
raise ValidateApiCallTimeoutError("OHSOME request timed out. Please try again.") from e

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop "Please try again"
This was not directly triggered by the user

if response.status_code != 200:
logger.warning(
"ohsome element count request failed: check for errors in filter or geometries",
Expand All @@ -210,16 +220,24 @@ def get_object_count_from_ohsome(area: str, ohsome_filter: PydanticLongText) ->
return int(value)


def ohsome(request: dict[str, Any], area: str, properties: str | None = None) -> dict[str, Any]:
def ohsome(
request: dict[str, Any],
area: str,
properties: str | None = None,
timeout: int = Config.DEFAULT_EXTERNAL_API_TIMEOUT,
) -> dict[str, Any]:
"""Request data from Ohsome API."""
url = Config.OHSOME_API_LINK + request["endpoint"]
data = {"bpolys": area, "filter": request["filter"]}
if properties:
data["properties"] = properties
logger.info("Target: %s", url)
logger.info("Filter: %s", request["filter"])
# FIXME(tnagorra): Need to check what the timeout should be

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the FIXME comment. Also the timeout has changed from 100

response = requests.post(url, data=data, timeout=100)
try:
response = requests.post(url, data=data, timeout=timeout)
except requests.exceptions.Timeout as e:
logger.warning("ohsome request timed out")
raise ValidateApiCallTimeoutError("OHSOME request timed out. Please try again.") from e
if response.status_code != 200:
logger.warning(
"ohsome request failed: check for errors in filter or geometries",
Expand Down
33 changes: 27 additions & 6 deletions project_types/validate/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
from main.config import Config
from project_types.base import project as base_project
from project_types.tile_map_service.base.project import create_json_dump
from project_types.validate.api_calls import ValidateApiCallError, get_object_count_from_ohsome, ohsome
from project_types.validate.api_calls import (
ValidateApiCallError,
ValidateApiCallTimeoutError,
get_object_count_from_ohsome,
ohsome,
)
from utils import fields as custom_fields
from utils.asset_types.models import AoiGeometryAssetProperty
from utils.common import Grouping, clean_up_none_keys, to_groups
Expand Down Expand Up @@ -157,9 +162,12 @@ def _validate_object_count(count: int | None) -> int:
def test_geojson_url_project(url: str):
logger.info("Fetching object geojson from %s", url)

# FIXME(frozenhelium): use predefined timeout duration
# FIXME(tnagorra): handle timeout error
response = requests.get(url, timeout=500)
try:
response = requests.get(url, timeout=Config.DEFAULT_EXTERNAL_API_TIMEOUT)
except requests.exceptions.Timeout as e:
raise base_project.ValidationException(
f"Failed to fetch object geojson from {url}: request timed out",
) from e
if response.status_code != 200:
raise base_project.ValidationException(
f"Failed to fetch object geojson from {url}",
Expand Down Expand Up @@ -207,6 +215,8 @@ def test_ohsome_objects_from_aoi_asset(
feature_collection.model_dump_json(),
ohsome_filter,
)
except ValidateApiCallTimeoutError as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do wr need this except when we already have a generic except that does the same thing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the generic exception is used for all the Exception. Added new exception just for the Timeout exception! Should we just use the generic one?

raise base_project.ValidationException(str(e)) from e
except Exception as e:
raise base_project.ValidationException("Failed to get object_count from ohsome") from e

Expand All @@ -223,7 +233,12 @@ def test_tasking_manager_project(
hot_tm_url = f"{Config.HOT_TASKING_MANAGER_PROJECT_API_LINK}projects/{hot_tm_id}/queries/aoi/?as_file=false"
logger.info("Fetching AOI geojson on HOT from %s", hot_tm_url)

aoi_result = requests.get(hot_tm_url, timeout=500)
try:
aoi_result = requests.get(hot_tm_url, timeout=Config.DEFAULT_EXTERNAL_API_TIMEOUT)
except requests.exceptions.Timeout as e:
raise base_project.ValidationException(
f"Failed to fetch AOI GeoJSON from HOT Tasking Manager for tm_id {hot_tm_id}: request timed out",
) from e
if aoi_result.status_code != 200:
raise base_project.ValidationException(
f"Failed to fetch AOI GeoJSON from HOT Tasking Manager for tm_id {hot_tm_id}",
Expand Down Expand Up @@ -269,6 +284,8 @@ def test_tasking_manager_project(
feature_collection.model_dump_json(),
ohsome_filter,
)
except ValidateApiCallTimeoutError as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this except when we already have a generic except that does the same thing

raise base_project.ValidationException(str(e)) from e
except Exception as e:
raise base_project.ValidationException("Failed to get object_count from ohsome") from e

Expand Down Expand Up @@ -311,9 +328,13 @@ def _get_object_geometry_from_ohsome(self, geojson: dict[typing.Any, typing.Any]
feature_collection.model_dump_json(),
properties="tags, metadata",
)
except ValidateApiCallTimeoutError as e:
raise base_project.ValidationException(str(e)) from e
except ValidateApiCallError as e:
# NOTE: Handles calls from OHSOME, OSMCHA and OSM
raise base_project.ValidationException("Failed to fetch data from OHSOME/OSMCHA/OSM") from e
raise base_project.ValidationException(
"Failed to fetch data from OHSOME/OSMCHA/OSM",
) from e
except requests.JSONDecodeError as e:
# NOTE: Handles calls from OHSOME and OSMCHA
# OSM responds in XML format
Expand Down
Loading