Skip to content

Commit 7f219c0

Browse files
authored
fix: drop stale DB connections around LaunchDarkly fetch phase (#7219)
1 parent de45b91 commit 7f219c0

File tree

4 files changed

+69
-26
lines changed

4 files changed

+69
-26
lines changed

api/integrations/launch_darkly/services.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from projects.tags.models import Tag
4242
from segments.models import Condition, Segment, SegmentRule
4343
from users.models import FFAdminUser
44+
from util.db import closing_stale_connections
4445
from util.util import iter_chunked_concat, truncate
4546

4647
logger = logging.getLogger(__name__)
@@ -1113,34 +1114,35 @@ def process_import_request(
11131114

11141115
ld_client = LaunchDarklyClient(ld_token)
11151116

1116-
try:
1117-
ld_environments = ld_client.get_environments(project_key=ld_project_key)
1118-
ld_flags = ld_client.get_flags_by_envs(
1119-
project_key=ld_project_key,
1120-
environment_keys=[env["key"] for env in ld_environments],
1121-
)
1122-
ld_flag_tags = ld_client.get_flag_tags()
1123-
# ld_segment_tags = ld_client.get_segment_tags()
1124-
# Keyed by (segment, environment)
1125-
ld_segments: list[tuple[ld_types.UserSegment, str]] = []
1126-
for env in ld_environments:
1127-
ld_segments_for_env = ld_client.get_segments(
1117+
with closing_stale_connections():
1118+
try:
1119+
ld_environments = ld_client.get_environments(project_key=ld_project_key)
1120+
ld_flags = ld_client.get_flags_by_envs(
11281121
project_key=ld_project_key,
1129-
environment_key=env["key"],
1122+
environment_keys=[env["key"] for env in ld_environments],
11301123
)
1131-
for segment in ld_segments_for_env:
1132-
ld_segments.append((segment, env["key"]))
1133-
1134-
except RequestException as exc:
1135-
_log_error(
1136-
import_request=import_request,
1137-
error_message=(
1138-
f"{exc.__class__.__name__} "
1139-
f"{str(exc.response.status_code) + ' ' if exc.response else ''}"
1140-
+ f"when requesting {getattr(exc.request, 'path_url', 'unknown')}"
1141-
),
1142-
)
1143-
raise
1124+
ld_flag_tags = ld_client.get_flag_tags()
1125+
# ld_segment_tags = ld_client.get_segment_tags()
1126+
# Keyed by (segment, environment)
1127+
ld_segments: list[tuple[ld_types.UserSegment, str]] = []
1128+
for env in ld_environments:
1129+
ld_segments_for_env = ld_client.get_segments(
1130+
project_key=ld_project_key,
1131+
environment_key=env["key"],
1132+
)
1133+
for segment in ld_segments_for_env:
1134+
ld_segments.append((segment, env["key"]))
1135+
1136+
except RequestException as exc:
1137+
_log_error(
1138+
import_request=import_request,
1139+
error_message=(
1140+
f"{exc.__class__.__name__} "
1141+
f"{str(exc.response.status_code) + ' ' if exc.response else ''}"
1142+
+ f"when requesting {getattr(exc.request, 'path_url', 'unknown')}"
1143+
),
1144+
)
1145+
raise
11441146

11451147
# Create environments
11461148
environments_by_ld_environment_key = _create_environments_from_ld(

api/tests/unit/integrations/launch_darkly/test_services.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def test_create_import_request__valid_project__returns_expected(
7777
(Timeout(), "Timeout when requesting /expected_path"),
7878
],
7979
)
80+
@pytest.mark.django_db(transaction=True)
8081
def test_process_import_request__api_error__expected_status(
8182
ld_client_mock: MagicMock,
8283
ld_client_class_mock: MagicMock,
@@ -100,6 +101,7 @@ def test_process_import_request__api_error__expected_status(
100101
assert import_request.status["error_messages"] == [expected_error_message]
101102

102103

104+
@pytest.mark.django_db(transaction=True)
103105
def test_process_import_request__success__expected_status( # type: ignore[no-untyped-def]
104106
project: Project,
105107
import_request: LaunchDarklyImportRequest,
@@ -259,6 +261,7 @@ def test_process_import_request__success__expected_status( # type: ignore[no-un
259261
[tag.label for tag in tagged_feature.tags.all()] == ["testtag", "testtag2"]
260262

261263

264+
@pytest.mark.django_db(transaction=True)
262265
def test_process_import_request__valid_segments__imports_correctly( # type: ignore[no-untyped-def]
263266
project: Project,
264267
import_request: LaunchDarklyImportRequest,
@@ -459,6 +462,7 @@ def test_process_import_request__valid_segments__imports_correctly( # type: ign
459462
assert trait_value == identity.identifier
460463

461464

465+
@pytest.mark.django_db(transaction=True)
462466
def test_process_import_request__valid_rules__imports_correctly( # type: ignore[no-untyped-def]
463467
project: Project,
464468
import_request: LaunchDarklyImportRequest,
@@ -555,6 +559,7 @@ def test_process_import_request__valid_rules__imports_correctly( # type: ignore
555559
}
556560

557561

562+
@pytest.mark.django_db(transaction=True)
558563
def test_process_import_request__large_segments__correctly_imported(
559564
request: pytest.FixtureRequest,
560565
ld_client_class_mock: MagicMock,

api/tests/unit/util/test_db.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pytest_mock import MockerFixture
2+
3+
from util.db import closing_stale_connections
4+
5+
6+
def test_closing_stale_connections__exit__calls_close_old_connections(
7+
mocker: MockerFixture,
8+
) -> None:
9+
# Given
10+
mock_close_old_connections = mocker.patch("util.db.close_old_connections")
11+
12+
# When
13+
with closing_stale_connections():
14+
pass
15+
16+
# Then
17+
mock_close_old_connections.assert_called_once_with()

api/util/db.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from collections.abc import Iterator
2+
from contextlib import contextmanager
3+
4+
from django.db import close_old_connections
5+
6+
7+
@contextmanager
8+
def closing_stale_connections() -> Iterator[None]:
9+
"""
10+
Close any stale DB connections when the wrapped block exits.
11+
12+
Intended for blocks that may hold a DB connection idle for long enough
13+
that the DB server (or an intermediate proxy) terminates it — e.g. an
14+
HTTP call to a slow third-party API preceding a write phase.
15+
"""
16+
try:
17+
yield
18+
finally:
19+
close_old_connections()

0 commit comments

Comments
 (0)