Skip to content

Commit f0d2639

Browse files
authored
Merge pull request #62 from steamcmd/fix-redis-stability
Fix for Redis instability and changes health checks to be used for alternative health and readiness checks.
2 parents d3156d5 + eab6856 commit f0d2639

3 files changed

Lines changed: 192 additions & 53 deletions

File tree

Dockerfile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,16 @@ COPY --chown=$USER:$USER src/ $HOME/
2929
##################### INSTALLATION END #####################
3030

3131
# Set default container command
32-
CMD exec gunicorn web:app --max-requests 3000 --max-requests-jitter 150 --workers $WORKERS --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT
32+
CMD exec gunicorn web:app \
33+
--max-requests 1000 \
34+
--max-requests-jitter 100 \
35+
--workers $WORKERS \
36+
--worker-class uvicorn.workers.UvicornWorker \
37+
--bind 0.0.0.0:$PORT \
38+
--timeout 30 \
39+
--keep-alive 2 \
40+
--worker-connections 1000 \
41+
--preload \
42+
--access-logfile - \
43+
--error-logfile - \
44+
--log-level info

src/utils/redis.py

Lines changed: 135 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,90 @@
11
import logging
22
import config
33
import redis
4+
from redis.connection import ConnectionPool
5+
import threading
6+
7+
# Global connection pool - thread-safe singleton
8+
_connection_pool = None
9+
_pool_lock = threading.Lock()
10+
11+
12+
def get_connection_pool():
13+
"""
14+
Get or create Redis connection pool (thread-safe singleton).
15+
"""
16+
global _connection_pool
17+
18+
if _connection_pool is None:
19+
with _pool_lock:
20+
if _connection_pool is None:
21+
try:
22+
# Connection pool configuration
23+
pool_kwargs = {
24+
"max_connections": 50,
25+
"retry_on_timeout": True,
26+
"socket_connect_timeout": 5,
27+
"socket_timeout": 5,
28+
"health_check_interval": 30,
29+
}
30+
31+
# try connection string, or default to separate REDIS_* env vars
32+
if config.redis_url:
33+
_connection_pool = ConnectionPool.from_url(
34+
config.redis_url, db=config.redis_database, **pool_kwargs
35+
)
36+
elif config.redis_password:
37+
_connection_pool = ConnectionPool(
38+
host=config.redis_host,
39+
port=config.redis_port,
40+
password=config.redis_password,
41+
db=config.redis_database,
42+
**pool_kwargs,
43+
)
44+
else:
45+
_connection_pool = ConnectionPool(
46+
host=config.redis_host,
47+
port=config.redis_port,
48+
db=config.redis_database,
49+
**pool_kwargs,
50+
)
51+
52+
logging.info("Redis connection pool created successfully")
53+
54+
except Exception as error:
55+
logging.error(f"Failed to create Redis connection pool: {error}")
56+
return None
57+
58+
return _connection_pool
459

560

661
def connect():
762
"""
8-
Parse redis config and connect.
63+
Get Redis connection from pool.
964
"""
65+
pool = get_connection_pool()
66+
if pool is None:
67+
return None
1068

1169
try:
12-
# try connection string, or default to separate REDIS_* env vars
13-
if config.redis_url:
14-
rds = redis.Redis.from_url(config.redis_url, db=config.redis_database)
15-
16-
elif config.redis_password:
17-
rds = redis.Redis(
18-
host=config.redis_host,
19-
port=config.redis_port,
20-
password=config.redis_password,
21-
db=config.redis_database,
22-
)
23-
else:
24-
rds = redis.Redis(
25-
host=config.redis_host, port=config.redis_port, db=config.redis_database
26-
)
27-
70+
return redis.Redis(connection_pool=pool)
2871
except Exception as error:
29-
logging.error("Failed to connect to Redis with error: " + error)
30-
return False
31-
32-
return rds
72+
logging.error(f"Failed to get Redis connection from pool: {error}")
73+
return None
3374

3475

3576
def read(key):
3677
"""
3778
Read specified key from Redis.
3879
"""
39-
4080
rds = connect()
4181

82+
if rds is None:
83+
logging.error(f"Failed to get Redis connection for key: {key}")
84+
return False
85+
4286
try:
43-
# get info from cache
87+
# get info from cache with timeout
4488
data = rds.get(key)
4589

4690
# return False if not found
@@ -53,25 +97,40 @@ def read(key):
5397
# return data from Redis
5498
return data
5599

100+
except redis.ConnectionError as conn_error:
101+
logging.error(
102+
f"Redis connection error while reading key {key}: {conn_error}",
103+
extra={"key": key, "error_type": "connection_error"},
104+
)
105+
return False
106+
except redis.TimeoutError as timeout_error:
107+
logging.error(
108+
f"Redis timeout error while reading key {key}: {timeout_error}",
109+
extra={"key": key, "error_type": "timeout_error"},
110+
)
111+
return False
56112
except Exception as redis_error:
57-
# print query parse error and return empty dict
58113
logging.error(
59-
"An error occured while trying to read and decode from Redis",
60-
extra={"key": key, "error_msg": redis_error},
114+
f"Unexpected error while reading from Redis key {key}: {redis_error}",
115+
extra={
116+
"key": key,
117+
"error_type": "unexpected_error",
118+
"error_msg": str(redis_error),
119+
},
61120
)
62-
logging.error(redis_error)
63-
64-
# return failed status
65-
return False
121+
return False
66122

67123

68124
def write(key, data):
69125
"""
70126
Write specified key to Redis.
71127
"""
72-
73128
rds = connect()
74129

130+
if rds is None:
131+
logging.error(f"Failed to get Redis connection for key: {key}")
132+
return False
133+
75134
# write data and set ttl
76135
try:
77136
expiration = int(config.cache_expiration)
@@ -82,43 +141,70 @@ def write(key, data):
82141
else:
83142
rds.set(key, data, ex=expiration)
84143

85-
# return succes status
144+
# return success status
86145
return True
87146

147+
except redis.ConnectionError as conn_error:
148+
logging.error(
149+
f"Redis connection error while writing key {key}: {conn_error}",
150+
extra={"key": key, "error_type": "connection_error"},
151+
)
152+
return False
153+
except redis.TimeoutError as timeout_error:
154+
logging.error(
155+
f"Redis timeout error while writing key {key}: {timeout_error}",
156+
extra={"key": key, "error_type": "timeout_error"},
157+
)
158+
return False
88159
except Exception as redis_error:
89-
# print query parse error and return empty dict
90160
logging.error(
91-
"An error occured while trying to write to Redis cache",
92-
extra={"key": key, "error_msg": redis_error},
161+
f"Unexpected error while writing to Redis key {key}: {redis_error}",
162+
extra={
163+
"key": key,
164+
"error_type": "unexpected_error",
165+
"error_msg": str(redis_error),
166+
},
93167
)
94-
logging.error(redis_error)
95-
96-
# return fail status
97-
return False
168+
return False
98169

99170

100171
def increment(key, amount=1):
101172
"""
102-
Increment value of amount to
103-
specified key to Redis.
173+
Increment value of amount to specified key in Redis.
104174
"""
105-
106175
rds = connect()
107176

177+
if rds is None:
178+
logging.error(f"Failed to get Redis connection for key: {key}")
179+
return False
180+
108181
# increment data of key
109182
try:
110183
# increment and set new value
111184
rds.incrby(key, amount)
112185

113-
# return succes status
186+
# return success status
114187
return True
115188

189+
except redis.ConnectionError as conn_error:
190+
logging.error(
191+
f"Redis connection error while incrementing key {key}: {conn_error}",
192+
extra={"key": key, "error_type": "connection_error"},
193+
)
194+
return False
195+
except redis.TimeoutError as timeout_error:
196+
logging.error(
197+
f"Redis timeout error while incrementing key {key}: {timeout_error}",
198+
extra={"key": key, "error_type": "timeout_error"},
199+
)
200+
return False
116201
except Exception as redis_error:
117-
# print query parse error and return empty dict
118202
logging.error(
119-
"An error occured while trying to increment value in Redis cache",
120-
extra={"key": key, "error_msg": redis_error},
203+
f"Unexpected error while incrementing Redis key {key}: {redis_error}",
204+
extra={
205+
"key": key,
206+
"error_type": "unexpected_error",
207+
"error_msg": str(redis_error),
208+
},
121209
)
122-
123-
# return fail status
124-
return False
210+
return False

src/web.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ def read_app(app_id: int, pretty: bool = False):
4242
logging.info(
4343
"App info could not be found in cache", extra={"apps": str([app_id])}
4444
)
45-
info = utils.steam.get_apps_info([app_id])
46-
data = json.dumps(info)
47-
utils.redis.write("app." + str(app_id), data)
4845
else:
4946
info = json.loads(info)
5047
logging.info(
@@ -95,3 +92,47 @@ def read_item(pretty: bool = False):
9592
"data": "Something went wrong while retrieving and parsing the current API version. Please try again later",
9693
"pretty": pretty,
9794
}
95+
96+
97+
@app.get("/health")
98+
def health_check():
99+
"""
100+
Health check endpoint for Kubernetes liveness and readiness probes.
101+
"""
102+
try:
103+
# Test Redis connection if cache is enabled
104+
if config.cache == "True":
105+
rds = utils.redis.connect()
106+
if rds is None:
107+
return {"status": "unhealthy", "error": "Redis connection failed"}, 503
108+
109+
# Test Redis with a simple ping
110+
rds.ping()
111+
112+
return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"}
113+
114+
except Exception as e:
115+
logging.error(f"Health check failed: {e}")
116+
return {"status": "unhealthy", "error": str(e)}, 503
117+
118+
119+
@app.get("/ready")
120+
def readiness_check():
121+
"""
122+
Readiness check endpoint for Kubernetes readiness probes.
123+
"""
124+
try:
125+
# Test Redis connection if cache is enabled
126+
if config.cache == "True":
127+
rds = utils.redis.connect()
128+
if rds is None:
129+
return {"status": "not_ready", "error": "Redis connection failed"}, 503
130+
131+
# Test Redis with a simple ping
132+
rds.ping()
133+
134+
return {"status": "ready"}
135+
136+
except Exception as e:
137+
logging.error(f"Readiness check failed: {e}")
138+
return {"status": "not_ready", "error": str(e)}, 503

0 commit comments

Comments
 (0)