Skip to content

Commit eef90fa

Browse files
fix(redis): don't use unbound variable [backport 1.20] (#7025)
Backport ce9d9c3 from #6994 to 1.20. The exception handling in Redis monkey patch in completely bugged, making Redis+ddtrace broken. This fixes it. Fixes #6993 ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. Co-authored-by: Mehdi ABAAKOUK <[email protected]>
1 parent 58ed1d5 commit eef90fa

File tree

7 files changed

+79
-12
lines changed

7 files changed

+79
-12
lines changed

ddtrace/contrib/redis/asyncio_patch.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,17 @@ async def traced_async_execute_pipeline(func, instance, args, kwargs):
3434

3535

3636
async def _run_redis_command_async(span, func, args, kwargs):
37+
parsed_command = stringify_cache_args(args)
38+
redis_command = parsed_command.split(" ")[0]
3739
try:
38-
parsed_command = stringify_cache_args(args)
39-
redis_command = parsed_command.split(" ")[0]
40-
4140
result = await func(*args, **kwargs)
41+
if redis_command in ROW_RETURNING_COMMANDS:
42+
determine_row_count(redis_command=redis_command, span=span, result=result)
4243
return result
4344
except Exception:
4445
if redis_command in ROW_RETURNING_COMMANDS:
4546
span.set_metric(db.ROWCOUNT, 0)
4647
raise
47-
finally:
48-
if redis_command in ROW_RETURNING_COMMANDS:
49-
determine_row_count(redis_command=redis_command, span=span, result=result)
5048

5149

5250
async def traced_async_execute_cluster_pipeline(func, instance, args, kwargs):

ddtrace/contrib/trace_utils_redis.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,17 @@ def determine_row_count(redis_command, span, result):
7676

7777

7878
def _run_redis_command(span, func, args, kwargs):
79+
parsed_command = stringify_cache_args(args)
80+
redis_command = parsed_command.split(" ")[0]
7981
try:
80-
parsed_command = stringify_cache_args(args)
81-
redis_command = parsed_command.split(" ")[0]
82-
8382
result = func(*args, **kwargs)
83+
if redis_command in ROW_RETURNING_COMMANDS:
84+
determine_row_count(redis_command=redis_command, span=span, result=result)
8485
return result
8586
except Exception:
8687
if redis_command in ROW_RETURNING_COMMANDS:
8788
span.set_metric(db.ROWCOUNT, 0)
8889
raise
89-
finally:
90-
if redis_command in ROW_RETURNING_COMMANDS:
91-
determine_row_count(redis_command=redis_command, span=span, result=result)
9290

9391

9492
@contextmanager

ddtrace/internal/compat.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
PY2 = sys.version_info[0] == 2
4747
PY3 = sys.version_info[0] == 3
4848

49+
try:
50+
from unittest import mock
51+
except ImportError:
52+
import mock # type: ignore # noqa
53+
4954
try:
5055
from builtin import TimeoutError
5156
except ImportError:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
redis: Resolves ``UnboundLocalError`` raised when a traced redis command raises an exception.

tests/contrib/redis/test_redis.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# -*- coding: utf-8 -*-
2+
import pytest
23
import redis
34

45
import ddtrace
56
from ddtrace import Pin
67
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
78
from ddtrace.contrib.redis.patch import patch
89
from ddtrace.contrib.redis.patch import unpatch
10+
from ddtrace.internal.compat import mock
911
from ddtrace.internal.schema import DEFAULT_SPAN_SERVICE_NAME
1012
from tests.opentracer.utils import init_tracer
1113
from tests.utils import DummyTracer
@@ -137,6 +139,15 @@ def test_basics(self):
137139
assert span.resource == "GET cheese"
138140
assert span.get_metric(ANALYTICS_SAMPLE_RATE_KEY) is None
139141

142+
def test_connection_error(self):
143+
with mock.patch.object(
144+
redis.connection.ConnectionPool,
145+
"get_connection",
146+
side_effect=redis.exceptions.ConnectionError("whatever"),
147+
):
148+
with pytest.raises(redis.exceptions.ConnectionError):
149+
self.r.get("foo")
150+
140151
def test_analytics_without_rate(self):
141152
with self.override_config("redis", dict(analytics_enabled=True)):
142153
us = self.r.get("cheese")

tests/contrib/redis/test_redis_asyncio.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing
2+
from unittest import mock
23

34
import pytest
45
import redis
@@ -77,6 +78,18 @@ async def test_unicode_request(redis_client):
7778
assert val is None
7879

7980

81+
@pytest.mark.asyncio
82+
@pytest.mark.snapshot(wait_for_num_traces=1, ignores=["meta.error.stack"])
83+
async def test_connection_error(redis_client):
84+
with mock.patch.object(
85+
redis.asyncio.connection.ConnectionPool,
86+
"get_connection",
87+
side_effect=redis.exceptions.ConnectionError("whatever"),
88+
):
89+
with pytest.raises(redis.exceptions.ConnectionError):
90+
await redis_client.get("foo")
91+
92+
8093
@pytest.mark.asyncio
8194
@pytest.mark.snapshot(wait_for_num_traces=2)
8295
async def test_decoding_non_utf8_args(redis_client):
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[[
2+
{
3+
"name": "redis.command",
4+
"service": "redis",
5+
"resource": "GET foo",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "redis",
10+
"error": 1,
11+
"meta": {
12+
"_dd.base_service": "",
13+
"_dd.p.dm": "-0",
14+
"component": "redis",
15+
"db.system": "redis",
16+
"error.message": "whatever",
17+
"error.stack": "Traceback (most recent call last):\n File \"/root/project/ddtrace/contrib/trace_utils_redis.py\", line 117, in _trace_redis_cmd\n yield span\n File \"/root/project/ddtrace/contrib/redis/asyncio_patch.py\", line 22, in traced_async_execute_command\n return await _run_redis_command_async(span=span, func=func, args=args, kwargs=kwargs)\n File \"/root/project/ddtrace/contrib/redis/asyncio_patch.py\", line 41, in _run_redis_command_async\n result = await func(*args, **kwargs)\n File \"/root/project/.riot/venv_py31011_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_pytest-asyncio_redis~41/lib/python3.10/site-packages/redis/asyncio/client.py\", line 509, in execute_command\n conn = self.connection or await pool.get_connection(command_name, **options)\n File \"/root/.pyenv/versions/3.10.11/lib/python3.10/unittest/mock.py\", line 2234, in _execute_mock_call\n raise effect\nredis.exceptions.ConnectionError: whatever\n",
18+
"error.type": "redis.exceptions.ConnectionError",
19+
"language": "python",
20+
"out.host": "127.0.0.1",
21+
"redis.raw_command": "GET foo",
22+
"runtime-id": "dc59875580884b52bebd2f9c402238f8",
23+
"span.kind": "client"
24+
},
25+
"metrics": {
26+
"_dd.measured": 1,
27+
"_dd.top_level": 1,
28+
"_dd.tracer_kr": 1.0,
29+
"_sampling_priority_v1": 1,
30+
"db.row_count": 0,
31+
"network.destination.port": 6379,
32+
"out.redis_db": 0,
33+
"process_id": 2340,
34+
"redis.args_length": 2
35+
},
36+
"duration": 935417,
37+
"start": 1695409673533997174
38+
}]]

0 commit comments

Comments
 (0)