Skip to content

Commit 5886cc2

Browse files
committed
fix(issues): Replace 'Internal Error' message on Postgres query timeout errors
Handle Postgres statement timeout errors on issue search which previously displayed 'Internal Error' but will now show more appropriate message explaining the query took too long and to possibly revise it Closes: #80166
1 parent 22e51b3 commit 5886cc2

File tree

3 files changed

+60
-3
lines changed

3 files changed

+60
-3
lines changed

src/sentry/api/utils.py

+11
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from datetime import timedelta
1010
from typing import Any, Literal, overload
1111

12+
import psycopg2.errorcodes
1213
import sentry_sdk
1314
from django.conf import settings
15+
from django.db.utils import OperationalError
1416
from django.http import HttpRequest
1517
from django.utils import timezone
1618
from rest_framework.exceptions import APIException, ParseError, Throttled
@@ -423,6 +425,15 @@ def handle_query_errors() -> Generator[None]:
423425
else:
424426
sentry_sdk.capture_exception(error)
425427
raise APIException(detail=message)
428+
except OperationalError as error:
429+
if hasattr(error, "pgcode") and error.pgcode == psycopg2.errorcodes.QUERY_CANCELED:
430+
if options.get("api.postgres-query-timeout-error-handling.enabled"):
431+
sentry_sdk.set_tag("query.error_reason", "Postgres statement timeout")
432+
raise Throttled(
433+
detail="Query timeout. Please try with a smaller date range or fewer conditions."
434+
)
435+
# Let other OperationalErrors propagate as normal
436+
raise
426437

427438

428439
def update_snuba_params_with_timestamp(

src/sentry/options/defaults.py

+8
Original file line numberDiff line numberDiff line change
@@ -2773,6 +2773,14 @@
27732773
flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
27742774
)
27752775

2776+
# Killswitch for Postgres query timeout error handling
2777+
register(
2778+
"api.postgres-query-timeout-error-handling.enabled",
2779+
default=False,
2780+
type=Bool,
2781+
flags=FLAG_MODIFIABLE_BOOL | FLAG_AUTOMATOR_MODIFIABLE,
2782+
)
2783+
27762784
# TODO: remove once removed from options
27772785
register(
27782786
"issue_platform.use_kafka_partition_key",

tests/sentry/api/test_utils.py

+41-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import unittest
33
from unittest.mock import MagicMock, patch
44

5+
import psycopg2
56
import pytest
7+
from django.db import OperationalError
68
from django.utils import timezone
7-
from rest_framework.exceptions import APIException
9+
from rest_framework.exceptions import APIException, Throttled
810
from sentry_sdk import Scope
911

1012
from sentry.api.utils import (
@@ -17,6 +19,7 @@
1719
from sentry.exceptions import IncompatibleMetricsQuery, InvalidParams, InvalidSearchQuery
1820
from sentry.testutils.cases import APITestCase
1921
from sentry.testutils.helpers.datetime import freeze_time
22+
from sentry.testutils.helpers.options import override_options
2023
from sentry.utils.snuba import (
2124
DatasetSelectionError,
2225
QueryConnectionFailed,
@@ -168,13 +171,12 @@ class FooBarError(Exception):
168171
pass
169172

170173

171-
class HandleQueryErrorsTest:
174+
class HandleQueryErrorsTest(APITestCase):
172175
@patch("sentry.api.utils.ParseError")
173176
def test_handle_query_errors(self, mock_parse_error):
174177
exceptions = [
175178
DatasetSelectionError,
176179
IncompatibleMetricsQuery,
177-
InvalidParams,
178180
InvalidSearchQuery,
179181
QueryConnectionFailed,
180182
QueryExecutionError,
@@ -198,6 +200,42 @@ def test_handle_query_errors(self, mock_parse_error):
198200
except Exception as e:
199201
assert isinstance(e, (FooBarError, APIException))
200202

203+
def test_handle_postgres_timeout(self):
204+
class TimeoutError(OperationalError):
205+
pgcode = psycopg2.errorcodes.QUERY_CANCELED
206+
207+
# Test when option is disabled (default)
208+
try:
209+
with handle_query_errors():
210+
raise TimeoutError()
211+
except Exception as e:
212+
assert isinstance(e, TimeoutError) # Should propagate original error
213+
214+
# Test when option is enabled
215+
with override_options({"api.postgres-query-timeout-error-handling.enabled": True}):
216+
try:
217+
with handle_query_errors():
218+
raise TimeoutError()
219+
except Exception as e:
220+
assert isinstance(e, Throttled)
221+
assert (
222+
str(e)
223+
== "Query timeout. Please try with a smaller date range or fewer conditions."
224+
)
225+
226+
@patch("sentry.api.utils.ParseError")
227+
def test_handle_other_operational_error(self, mock_parse_error):
228+
class OtherError(OperationalError):
229+
# No pgcode attribute
230+
pass
231+
232+
try:
233+
with handle_query_errors():
234+
raise OtherError()
235+
except Exception as e:
236+
assert isinstance(e, OtherError) # Should propagate original error
237+
mock_parse_error.assert_not_called()
238+
201239

202240
class ClampDateRangeTest(unittest.TestCase):
203241
def test_no_clamp_if_range_under_max(self):

0 commit comments

Comments
 (0)