GitHub Actions commited on
Commit
6b31aee
·
1 Parent(s): 5ec0ee0

Deploy backend from GitHub 35f3a2f883b10b3373c0b0501ee873f8e5f72d2d

Browse files
Files changed (1) hide show
  1. backend/app/core/redis.py +66 -17
backend/app/core/redis.py CHANGED
@@ -1,6 +1,11 @@
1
  """
2
  Redis-based rate limiter using a sliding window approach.
3
  Env vars: REDIS_URL, RATE_LIMIT_PER_MINUTE
 
 
 
 
 
4
  """
5
  import time
6
  from typing import Optional
@@ -8,44 +13,88 @@ from typing import Optional
8
  import redis.asyncio as aioredis
9
 
10
  from backend.app.core.config import settings
 
 
 
11
 
12
  _redis_client: Optional[aioredis.Redis] = None
 
 
 
 
13
 
 
 
14
 
15
- async def get_redis() -> aioredis.Redis:
16
- global _redis_client
 
 
 
17
  if _redis_client is None:
18
- _redis_client = aioredis.from_url(
19
- settings.REDIS_URL, decode_responses=True, socket_connect_timeout=5
20
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  return _redis_client
22
 
23
 
24
  async def check_rate_limit(key: str, limit: int = 0, window: int = 60) -> bool:
25
  """
26
  Returns True if under limit, False if rate-limited.
27
- Uses sorted set with timestamps for sliding window.
28
  """
 
 
 
29
  if limit <= 0:
30
  limit = settings.RATE_LIMIT_PER_MINUTE
31
- r = await get_redis()
32
  now = time.time()
33
  window_start = now - window
34
- pipe = r.pipeline()
35
- pipe.zremrangebyscore(key, 0, window_start)
36
- pipe.zadd(key, {str(now): now})
37
- pipe.zcard(key)
38
- pipe.expire(key, window + 1)
39
- results = await pipe.execute()
40
- count = results[2]
41
- return count <= limit
 
 
 
 
42
 
43
 
44
  async def get_cached(key: str) -> Optional[str]:
45
  r = await get_redis()
46
- return await r.get(key)
 
 
 
 
 
47
 
48
 
49
  async def set_cached(key: str, value: str, ttl: int = 300):
50
  r = await get_redis()
51
- await r.setex(key, ttl, value)
 
 
 
 
 
 
1
  """
2
  Redis-based rate limiter using a sliding window approach.
3
  Env vars: REDIS_URL, RATE_LIMIT_PER_MINUTE
4
+
5
+ If REDIS_URL is missing or invalid the module degrades gracefully:
6
+ - rate limiting is disabled (always returns True / under limit)
7
+ - caching is disabled (always returns None)
8
+ This prevents a bad/missing secret from crashing /api/analyze.
9
  """
10
  import time
11
  from typing import Optional
 
13
  import redis.asyncio as aioredis
14
 
15
  from backend.app.core.config import settings
16
+ from backend.app.core.logging import get_logger
17
+
18
+ logger = get_logger(__name__)
19
 
20
  _redis_client: Optional[aioredis.Redis] = None
21
+ _redis_broken: bool = False # set True once we know Redis is unavailable
22
+
23
+ VALID_SCHEMES = ("redis://", "rediss://", "unix://")
24
+
25
 
26
+ def _is_valid_redis_url(url: str) -> bool:
27
+ return any(url.startswith(scheme) for scheme in VALID_SCHEMES)
28
 
29
+
30
+ async def get_redis() -> Optional[aioredis.Redis]:
31
+ global _redis_client, _redis_broken
32
+ if _redis_broken:
33
+ return None
34
  if _redis_client is None:
35
+ url = settings.REDIS_URL
36
+ if not _is_valid_redis_url(url):
37
+ logger.warning(
38
+ "REDIS_URL has an invalid scheme – rate limiting and caching disabled. "
39
+ f"Expected redis://, rediss://, or unix:// but got: {url[:40]!r}"
40
+ )
41
+ _redis_broken = True
42
+ return None
43
+ try:
44
+ _redis_client = aioredis.from_url(
45
+ url, decode_responses=True, socket_connect_timeout=5
46
+ )
47
+ # Verify the connection is actually reachable
48
+ await _redis_client.ping()
49
+ except Exception as e:
50
+ logger.warning("Redis unavailable – rate limiting and caching disabled", error=str(e))
51
+ _redis_client = None
52
+ _redis_broken = True
53
+ return None
54
  return _redis_client
55
 
56
 
57
  async def check_rate_limit(key: str, limit: int = 0, window: int = 60) -> bool:
58
  """
59
  Returns True if under limit, False if rate-limited.
60
+ Always returns True (allow) when Redis is unavailable.
61
  """
62
+ r = await get_redis()
63
+ if r is None:
64
+ return True # degrade gracefully – allow the request
65
  if limit <= 0:
66
  limit = settings.RATE_LIMIT_PER_MINUTE
 
67
  now = time.time()
68
  window_start = now - window
69
+ try:
70
+ pipe = r.pipeline()
71
+ pipe.zremrangebyscore(key, 0, window_start)
72
+ pipe.zadd(key, {str(now): now})
73
+ pipe.zcard(key)
74
+ pipe.expire(key, window + 1)
75
+ results = await pipe.execute()
76
+ count = results[2]
77
+ return count <= limit
78
+ except Exception as e:
79
+ logger.warning("Redis rate-limit check failed – allowing request", error=str(e))
80
+ return True
81
 
82
 
83
  async def get_cached(key: str) -> Optional[str]:
84
  r = await get_redis()
85
+ if r is None:
86
+ return None
87
+ try:
88
+ return await r.get(key)
89
+ except Exception:
90
+ return None
91
 
92
 
93
  async def set_cached(key: str, value: str, ttl: int = 300):
94
  r = await get_redis()
95
+ if r is None:
96
+ return
97
+ try:
98
+ await r.setex(key, ttl, value)
99
+ except Exception:
100
+ pass