diff --git a/Dockerfile b/Dockerfile index be8306d..692769d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.13.6-alpine@sha256:f196fd275fdad7287ccb4b0a85c2e402bb8c794d205cf6158909041c1ee9f38d +FROM python:3.13.6-alpine@sha256:af1fd7a973d8adc761ee6b9d362b99329b39eb096ea3c53b8838f99bd187011e RUN addgroup -S appgroup && adduser -S -G appgroup appuser @@ -17,7 +17,9 @@ ENV FLASK_APP=app.main.py:app \ CACHE_TTL=300 \ MINIO_PORT=9000 \ MINIO_ACCESS_KEY=minioadmin \ - MINIO_SECRET_KEY=minioadmin + MINIO_SECRET_KEY=minioadmin \ + REDIS_HOST=redis \ + MINIO_HOST=minio USER appuser diff --git a/app/readiness.py b/app/readiness.py index 86167cd..a21807c 100644 --- a/app/readiness.py +++ b/app/readiness.py @@ -1,4 +1,6 @@ '''Module to check the readiness of the stored information''' +import requests +import redis from app.opensense import get_temperature from app.config import create_redis_client @@ -9,30 +11,61 @@ def check_caching(): if not REDIS_AVAILABLE: return True - cache_key = "temperature_data" - ttl = redis_client.ttl(cache_key) + try: + cache_key = "temperature_data" + ttl = redis_client.ttl(cache_key) - if ttl in (-2, -1): - return True + if ttl in (-2, -1): + return True - return False + return False + except redis.RedisError as e: + print(f"Redis error while checking cache: {e}") + return True def reachable_boxes(): - '''Check if 50% + 1 of boxes are reachable''' - _, sensor_stats = get_temperature() - total = sensor_stats["total_sensors"] - null_count = sensor_stats["null_count"] - if total > 0 and null_count > (total * 0.5): - print("Warning: More than 50% of sensors are unreachable") + '''Check if more than 50% of sensor boxes are reachable''' + try: + _, sensor_stats = get_temperature() + total_boxes = sensor_stats.get('total_sensors', 0) + unreachable = sensor_stats.get('null_count', 0) + + # No sensors configured => treat as healthy + if total_boxes == 0: + return 200 + + percentage_unreachable = (unreachable / total_boxes) * 100 + + # Fail only if strictly more than 50% are unreachable + if percentage_unreachable > 50: + return 400 + return 200 + + except requests.exceptions.RequestException as e: + # Handle network-related errors from the API call + print(f"Network error checking reachable boxes: {e}") + return 200 + except redis.RedisError as e: + # Handle Redis-related errors + print(f"Redis error checking reachable boxes: {e}") + return 200 + except (ValueError, TypeError, KeyError) as e: + # Handle data parsing errors + print(f"Data error checking reachable boxes: {e}") return 400 - return 200 def readiness_check(): '''Combined readiness check for the /readyz endpoint''' - boxes_status = reachable_boxes() - cache_is_old = check_caching() + try: + boxes_status = reachable_boxes() + cache_is_old = check_caching() - if boxes_status == 400 and cache_is_old: - return 503 + # Only fail if BOTH conditions are bad + if boxes_status == 400 and cache_is_old: + return 503 - return 200 + return 200 + except redis.RedisError as e: + # If Redis is completely unavailable, still allow the service to be ready + print(f"Redis error during readiness check: {e}") + return 200 diff --git a/k8s/deployment.yml b/k8s/deployment.yml index f5cee57..82fd257 100644 --- a/k8s/deployment.yml +++ b/k8s/deployment.yml @@ -58,7 +58,7 @@ spec: port: 5000 initialDelaySeconds: 30 periodSeconds: 10 - timeoutSeconds: 5 + timeoutSeconds: 300 failureThreshold: 3 volumeMounts: - name: tmp-volume diff --git a/tests/test_modules.py b/tests/test_modules.py index 43e655d..9521b83 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -2,6 +2,8 @@ import re import unittest import unittest.mock as mock +import requests # added +import redis # added from minio.error import S3Error, InvalidResponseError from app.storage import store_temperature_data from app.main import app @@ -32,8 +34,10 @@ def test_version_endpoint(self): def test_temperature_endpoint(self): """Test temperature endpoint returns 200 or 500""" - response = self.client.get('/temperature') - self.assertIn(response.status_code, [200, 500]) + with mock.patch('app.opensense.requests.get', + return_value=MockOpenSenseResponse(20)): + response = self.client.get('/temperature') + self.assertIn(response.status_code, [200, 500]) def test_metrics_endpoint(self): """Test metrics endpoint returns 200""" @@ -94,7 +98,9 @@ class TestOpenSense(unittest.TestCase): def test_get_temperature_returns_tuple(self): """Test that opensense.get_temperature returns a tuple with correct format""" - result, stats = opensense.get_temperature() + with mock.patch('app.opensense.requests.get', + return_value=MockOpenSenseResponse(20)): + result, stats = opensense.get_temperature() self.assertIsInstance(result, str) self.assertIsInstance(stats, dict) self.assertIn('total_sensors', stats) @@ -293,6 +299,17 @@ def test_check_caching_fresh_cache(self): result = readiness.check_caching() self.assertFalse(result) # Cache is fresh + def test_check_caching_redis_error(self): + """TTL raises RedisError -> treated as old cache (True)""" + mock_redis_client = mock.MagicMock() + mock_redis_client.ttl.side_effect = redis.RedisError("boom") + + with mock.patch('app.readiness.REDIS_AVAILABLE', True), \ + mock.patch('app.readiness.redis_client', mock_redis_client), \ + mock.patch('builtins.print'): + result = readiness.check_caching() + self.assertTrue(result) + def test_reachable_boxes_healthy(self): """Test reachable_boxes when most sensors are working""" mock_stats = {"total_sensors": 100, "null_count": 10} @@ -324,6 +341,36 @@ def test_reachable_boxes_edge_cases(self): return_value=("temp", {"total_sensors": 100, "null_count": 50})): self.assertEqual(readiness.reachable_boxes(), 200) + def test_reachable_boxes_network_error(self): + """requests exceptions -> treated as healthy (200)""" + with mock.patch('app.readiness.get_temperature', + side_effect=requests.exceptions.RequestException("net")), \ + mock.patch('builtins.print'): + self.assertEqual(readiness.reachable_boxes(), 200) + + def test_reachable_boxes_redis_error(self): + """Redis errors inside get_temperature -> treated as healthy (200)""" + with mock.patch('app.readiness.get_temperature', + side_effect=redis.RedisError("redis down")), \ + mock.patch('builtins.print'): + self.assertEqual(readiness.reachable_boxes(), 200) + + def test_reachable_boxes_data_error(self): + """Data parsing error -> returns 400""" + # Cause a TypeError during percentage calculation + bad_stats = {"total_sensors": 2, "null_count": "x"} + with mock.patch('app.readiness.get_temperature', + return_value=("temp", bad_stats)), \ + mock.patch('builtins.print'): + self.assertEqual(readiness.reachable_boxes(), 400) + + def test_readiness_check_redis_error_top_level(self): + """Top-level Redis error in readiness_check -> returns 200""" + with mock.patch('app.readiness.check_caching', + side_effect=redis.RedisError("ttl failed")), \ + mock.patch('builtins.print'): + self.assertEqual(readiness.readiness_check(), 200) + def test_readiness_check_all_good(self): """Test readiness_check when everything is healthy""" with mock.patch('app.readiness.check_caching', return_value=False), \