Skip to content

Commit eac61ed

Browse files
authored
improved redis singleton (#106)
1 parent 5dbf134 commit eac61ed

File tree

2 files changed

+48
-24
lines changed

2 files changed

+48
-24
lines changed

src/backend/cache/redis_client.py

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import asyncio
23
from redis import asyncio as aioredis
34
from dotenv import load_dotenv
45

@@ -15,30 +16,55 @@ class RedisClient:
1516
"""Service for managing Redis connections with proper lifecycle management."""
1617

1718
_instance = None
19+
_client = None
20+
_lock = asyncio.Lock()
1821

1922
@classmethod
2023
async def get_instance(cls) -> aioredis.Redis:
21-
"""Get or create a Redis client instance."""
22-
if cls._instance is None:
23-
cls._instance = cls()
24-
await cls._instance.initialize()
25-
return cls._instance.client
24+
"""Get or create a Redis client instance with proper singleton behavior."""
25+
if cls._client is None:
26+
async with cls._lock:
27+
# Double-check pattern to prevent race conditions
28+
if cls._client is None:
29+
if cls._instance is None:
30+
cls._instance = cls()
31+
await cls._instance.initialize()
32+
return cls._client
2633

27-
def __init__(self):
28-
self.client = None
2934

3035
async def initialize(self) -> None:
31-
"""Initialize the Redis client."""
32-
self.client = aioredis.from_url(
33-
REDIS_URL,
34-
password=REDIS_PASSWORD,
35-
decode_responses=True,
36-
health_check_interval=30
37-
)
38-
39-
async def close(self) -> None:
40-
"""Close the Redis client connection."""
41-
if self.client:
42-
await self.client.close()
43-
self.client = None
44-
print("Redis client closed.")
36+
"""Initialize the Redis client with connection pool limits."""
37+
if RedisClient._client is None:
38+
try:
39+
RedisClient._client = aioredis.from_url(
40+
REDIS_URL,
41+
password=REDIS_PASSWORD,
42+
decode_responses=True,
43+
health_check_interval=30,
44+
max_connections=20, # Limit connection pool size
45+
retry_on_timeout=True,
46+
socket_keepalive=True,
47+
socket_keepalive_options={}
48+
)
49+
print(f"Redis client initialized with connection pool (max 20 connections)")
50+
51+
# Test the connection
52+
await RedisClient._client.ping()
53+
54+
except Exception as e:
55+
print(f"Failed to initialize Redis client: {e}")
56+
RedisClient._client = None
57+
raise
58+
59+
@classmethod
60+
async def close(cls) -> None:
61+
"""Close the Redis client connection and reset singleton state."""
62+
if cls._client:
63+
try:
64+
await cls._client.close()
65+
print("Redis client closed.")
66+
except Exception as e:
67+
print(f"Error closing Redis client: {e}")
68+
finally:
69+
cls._client = None
70+
cls._instance = None

src/backend/main.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ async def lifespan(app: FastAPI):
4949

5050
# Initialize Redis client and verify connection
5151
redis = await RedisClient.get_instance()
52-
await redis.ping()
5352
print("Redis connection established successfully")
5453

5554
# Initialize the canvas worker
@@ -58,9 +57,8 @@ async def lifespan(app: FastAPI):
5857

5958
yield
6059

61-
# Shutdown
6260
await CanvasWorker.shutdown_instance()
63-
await redis.close()
61+
await RedisClient.close()
6462
await engine.dispose()
6563

6664
app = FastAPI(lifespan=lifespan)

0 commit comments

Comments
 (0)