Skip to content

Commit d7f3c75

Browse files
committed
refactor: simplify enketo Redis client — remove connection pooling and fix pylint issues
Remove manual ConnectionPool management (redis.Redis handles pooling internally) and create a fresh client per call. Narrow broad exception catches to specific Redis/OS errors and eliminate global statement.
1 parent 2d73cfd commit d7f3c75

File tree

5 files changed

+41
-74
lines changed

5 files changed

+41
-74
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: CI
3-
on: # yamllint disable-line rule:truthy
3+
on: # yamllint disable-line rule:truthy
44
pull_request:
55
branches: ["*"]
66
push:
@@ -66,17 +66,17 @@ jobs:
6666
matrix:
6767
test_path:
6868
- [
69-
" Django Unit Tests (Libraries, Main, RestServices, SMS Support, Viewer, Messaging)",
70-
"python manage.py test onadata/libs onadata/apps/main onadata/apps/restservice onadata/apps/sms_support onadata/apps/viewer onadata/apps/messaging --noinput --timing --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4",
71-
]
69+
" Django Unit Tests (Libraries, Main, RestServices, SMS Support, Viewer, Messaging)",
70+
"python manage.py test onadata/libs onadata/apps/main onadata/apps/restservice onadata/apps/sms_support onadata/apps/viewer onadata/apps/messaging --noinput --timing --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4",
71+
]
7272
- [
73-
"Django Unit Tests API",
74-
"python manage.py test onadata/apps/api --noinput --timing --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4",
75-
]
73+
"Django Unit Tests API",
74+
"python manage.py test onadata/apps/api --noinput --timing --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4",
75+
]
7676
- [
77-
"Django Unit Tests Logger",
78-
"python manage.py test onadata/apps/logger --noinput --timing --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4",
79-
]
77+
"Django Unit Tests Logger",
78+
"python manage.py test onadata/apps/logger --noinput --timing --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4",
79+
]
8080
name: "${{ matrix.test_path[0] }}"
8181
runs-on: ubuntu-22.04
8282
needs: static-analysis
@@ -184,7 +184,7 @@ jobs:
184184
optional_packages=PyYAML django-redis ${{ secrets.ECR_OPTIONAL_PACKAGES }}
185185
186186
- name: Install Trivy
187-
uses: aquasecurity/setup-trivy@v0.2.4
187+
uses: aquasecurity/setup-trivy@v0.2.6
188188
if: github.event_name == 'pull_request' || github.event_name == 'push'
189189
with:
190190
version: v0.69.1

onadata/apps/api/viewsets/xform_viewset.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,6 @@ def enketo_links(self, request, **kwargs):
531531
self.object = self.get_object()
532532
show_preview = request.GET.get("show_preview") == "true"
533533

534-
# --- try cache first ---
535534
if show_preview:
536535
cached = get_cached_preview_url(self.object.pk)
537536
if cached:
@@ -541,13 +540,11 @@ def enketo_links(self, request, **kwargs):
541540
if cached:
542541
return Response(self._build_survey_response(cached))
543542

544-
# --- cache miss: call Enketo API ---
545543
result = self._fetch_enketo_urls(request)
546544
if isinstance(result, Response):
547545
return result
548546
enketo_urls = result
549547

550-
# --- store in cache ---
551548
if show_preview:
552549
preview_url = enketo_urls.get("preview_url", "")
553550
if preview_url:

onadata/libs/utils/enketo_redis.py

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,17 @@
2929
# Default TTL: 24 hours. Override via ENKETO_LINKS_CACHE_TTL (seconds).
3030
DEFAULT_CACHE_TTL = 86400
3131

32-
# Module-level connection pool — shared across all requests.
33-
_pool = None
3432

35-
36-
def _get_pool():
37-
"""Return the shared ``ConnectionPool``, creating it on first call."""
38-
global _pool # noqa: PLW0603
39-
if _pool is not None:
40-
return _pool
33+
def _get_client():
34+
"""Return a new Redis client, or *None* if not configured."""
4135
url = getattr(settings, "ENKETO_LINKS_REDIS_URL", None)
4236
if not url:
4337
return None
4438
try:
45-
_pool = redis.ConnectionPool.from_url(url, decode_responses=True)
46-
return _pool
47-
except Exception:
48-
logger.exception("Failed to create enketo-links Redis connection pool")
49-
return None
50-
51-
52-
def _get_connection():
53-
"""Return a Redis client backed by the shared connection pool, or *None*."""
54-
pool = _get_pool()
55-
if pool is None:
39+
return redis.Redis.from_url(url, decode_responses=True)
40+
except (redis.ConnectionError, redis.RedisError, OSError):
41+
logger.exception("Failed to create enketo-links Redis client")
5642
return None
57-
return redis.Redis(connection_pool=pool)
5843

5944

6045
def _cache_ttl():
@@ -68,13 +53,13 @@ def get_cached_survey_urls(form_pk):
6853
``preview_url``, ``single_once_url``) or ``None`` on cache miss /
6954
error.
7055
"""
71-
conn = _get_connection()
72-
if conn is None:
56+
client = _get_client()
57+
if client is None:
7358
return None
7459
try:
75-
data = conn.hgetall(f"{SURVEY_URLS_PREFIX}{int(form_pk)}")
60+
data = client.hgetall(f"{SURVEY_URLS_PREFIX}{int(form_pk)}")
7661
return data if data else None
77-
except Exception:
62+
except (redis.ConnectionError, redis.RedisError, OSError):
7863
logger.exception("enketo-links Redis read error")
7964
return None
8065

@@ -85,43 +70,43 @@ def store_survey_urls(form_pk, urls):
8570
*urls* is a dict as returned by the Enketo API (keys like
8671
``offline_url``, ``preview_url``, ``single_once_url``).
8772
"""
88-
conn = _get_connection()
89-
if conn is None:
73+
client = _get_client()
74+
if client is None:
9075
return
9176
try:
9277
key = f"{SURVEY_URLS_PREFIX}{int(form_pk)}"
9378
mapping = {k: str(v) for k, v in urls.items() if v is not None}
9479
if mapping:
95-
pipe = conn.pipeline()
80+
pipe = client.pipeline()
9681
pipe.hset(key, mapping=mapping)
9782
pipe.expire(key, _cache_ttl())
9883
pipe.execute()
99-
except Exception:
84+
except (redis.ConnectionError, redis.RedisError, OSError):
10085
logger.exception("enketo-links Redis write error")
10186

10287

10388
def get_cached_preview_url(form_pk):
10489
"""Read the cached preview URL for *form_pk*, or ``None``."""
105-
conn = _get_connection()
106-
if conn is None:
90+
client = _get_client()
91+
if client is None:
10792
return None
10893
try:
109-
return conn.get(f"{PREVIEW_URL_PREFIX}{int(form_pk)}")
110-
except Exception:
94+
return client.get(f"{PREVIEW_URL_PREFIX}{int(form_pk)}")
95+
except (redis.ConnectionError, redis.RedisError, OSError):
11196
logger.exception("enketo-links Redis read error")
11297
return None
11398

11499

115100
def store_preview_url(form_pk, url):
116101
"""Cache just the preview URL for *form_pk*."""
117-
conn = _get_connection()
118-
if conn is None:
102+
client = _get_client()
103+
if client is None:
119104
return
120105
try:
121-
conn.set(
106+
client.set(
122107
f"{PREVIEW_URL_PREFIX}{int(form_pk)}",
123108
str(url),
124109
ex=_cache_ttl(),
125110
)
126-
except Exception:
111+
except (redis.ConnectionError, redis.RedisError, OSError):
127112
logger.exception("enketo-links Redis write error")

onadata/libs/utils/tests/test_enketo_redis.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from unittest.mock import MagicMock, patch
44

5+
import redis
56
from django.test import TestCase, override_settings
67

78
from onadata.libs.utils import enketo_redis
@@ -15,20 +16,9 @@
1516
)
1617

1718

18-
def _reset_pool():
19-
"""Reset the module-level connection pool between tests."""
20-
enketo_redis._pool = None
21-
22-
2319
class EnketoRedisDisabledTest(TestCase):
2420
"""When ENKETO_LINKS_REDIS_URL is empty, all functions are no-ops."""
2521

26-
def setUp(self):
27-
_reset_pool()
28-
29-
def tearDown(self):
30-
_reset_pool()
31-
3222
@override_settings(ENKETO_LINKS_REDIS_URL="")
3323
def test_get_cached_survey_urls_returns_none(self):
3424
self.assertIsNone(get_cached_survey_urls(42))
@@ -51,24 +41,16 @@ class EnketoRedisCacheTest(TestCase):
5141
"""Test caching with a mocked Redis connection."""
5242

5343
def setUp(self):
54-
_reset_pool()
5544
self.mock_redis = MagicMock()
56-
self.pool_patcher = patch.object(
45+
self.client_patcher = patch.object(
5746
enketo_redis,
58-
"_get_pool",
59-
return_value=MagicMock(),
60-
)
61-
self.redis_patcher = patch(
62-
"onadata.libs.utils.enketo_redis.redis.Redis",
47+
"_get_client",
6348
return_value=self.mock_redis,
6449
)
65-
self.pool_patcher.start()
66-
self.redis_patcher.start()
50+
self.client_patcher.start()
6751

6852
def tearDown(self):
69-
self.pool_patcher.stop()
70-
self.redis_patcher.stop()
71-
_reset_pool()
53+
self.client_patcher.stop()
7254

7355
# --- survey URLs ---
7456

@@ -89,7 +71,7 @@ def test_get_cached_survey_urls_returns_none_on_miss(self):
8971
self.assertIsNone(get_cached_survey_urls(99))
9072

9173
def test_get_cached_survey_urls_returns_none_on_error(self):
92-
self.mock_redis.hgetall.side_effect = Exception("boom")
74+
self.mock_redis.hgetall.side_effect = redis.RedisError("boom")
9375
self.assertIsNone(get_cached_survey_urls(99))
9476

9577
def test_store_survey_urls_writes_hash_with_ttl(self):

onadata/libs/utils/viewer_tools.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ def get_form_url(
297297
provided then Enketo will request the form list from
298298
https://example.com/[username]/formList. Same applies for preview if
299299
preview is True and also to a single form when xform_pk is provided.
300+
301+
When *protocol* is ``None`` (the default) it is read from the
302+
``ENKETO_PROTOCOL`` Django setting, falling back to ``"https"``.
300303
"""
301304
if protocol is None:
302305
protocol = getattr(settings, "ENKETO_PROTOCOL", "https")

0 commit comments

Comments
 (0)