Skip to content

fix(redis): tear down half-open client socket + alert on stuck Redis - #2185

Open
overthelex wants to merge 1 commit into
mainfrom
fix/redis-client-hardening-and-alerts
Open

fix(redis): tear down half-open client socket + alert on stuck Redis#2185
overthelex wants to merge 1 commit into
mainfrom
fix/redis-client-hardening-and-alerts

Conversation

@overthelex

@overthelex overthelex commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Проблема (prod-інцидент 2026-07)

Після blue-green cutover постійний node-redis клієнт бекенда перейшов у half-open стан: TCP-сокет мертвий, але node-redis вважав з'єднання «connected» і не розривав його. Reconnect не спрацьовував, тому кожна операція cache / rate-limit / chat-search падала в 2500ms-таймаут CacheAdapter безкінечно (6+ хв), поки бекенд не перезапустили вручну. У UI це проявлялося як «Помилка мережі. Перевірте підключення.»

Наявні keepAlive + pingInterval (LEXAI-1795) самі по собі не рятували: pingInterval шле PING, але без read-таймауту node-redis ніколи не закриває напівживий сокет, а TCP-keepAlive виявляє це надто повільно.

Виправлення

  • redis-client.ts: додано socket.socketTimeout = 60s — вікно без вхідних байтів закриває мертвий сокет і запускає reconnectStrategy. На живому з'єднанні PONG від pingInterval (знижено 30s→20s) тримає трафік значно нижче таймауту, тож спрацьовує лише на реально мертвому сокеті. Події ready/reconnecting/end під'єднані до хука стану.
  • cache-adapter.ts: setMetricsCallback — викликається на кожній таймаут/помилці операції.
  • metrics-service.ts: backend_redis_client_up (gauge) + redis_command_errors_total (counter). Назва gauge відрізняється від серверного redis_up з redis_exporter.
  • http-server.ts: під'єднання gauge (через хук стану) і лічильника помилок.
  • alert-rules.yml: два алерти:
    • BackendRedisClientDownbackend_redis_client_up == 0 за 2 хв (critical)
    • BackendRedisCommandErrorsrate(redis_command_errors_total) > 0.2/s за 3 хв (critical). Саме цей алерт ловить застряглий клієнт, поки gauge ще показує «up».

Тести

  • cache-adapter.test.ts: +2 кейси на metrics callback (спрацьовує з лейблом операції на помилці; не спрацьовує на успіху). Усі 6 тестів проходять.
  • TS: змінені файли компілюються без помилок.

Поза скоупом

  • redis-consultation-message-bus.ts (pub/sub) не чіпав — socketTimeout на subscriber-з'єднанні має нюанси, а інцидент був у cache-клієнті.

🤖 Generated with Claude Code


Summary by cubic

Hardened the backend redis client to auto-tear half-open sockets and reconnect. Added metrics and alerts to detect a stuck client and prevent cascading cache/rate-limit timeouts.

  • Bug Fixes

    • Add 60s socketTimeout and lower pingInterval to 20s to close dead sockets and trigger reconnects.
    • Emit connection state via a hook on ready/reconnecting/end.
  • New Features

    • Metrics: backend_redis_client_up gauge and redis_command_errors_total counter; wired in http-server via the state hook and CacheAdapter.setMetricsCallback; tests added for the callback.
    • Alerts: BackendRedisClientDown (2m) and BackendRedisCommandErrors (>0.2/s for 3m).

Written for commit b7c0067. Summary will update on new commits.

Review in cubic

Prod incident 2026-07: the backend's node-redis client went half-open
during a blue-green cutover. node-redis kept it "connected" but never
tore the dead socket down, so it never reconnected and EVERY cache /
rate-limit / chat-search op hit the 2500ms CacheAdapter timeout
indefinitely (6+ min) until a manual restart. keepAlive + pingInterval
(already present) were not enough on their own.

Fix + observability:
- redis-client: add socket.socketTimeout (60s) so a read-inactivity
  window closes a dead socket and triggers reconnectStrategy; PONG
  traffic from pingInterval (lowered 30s->20s) keeps a healthy link
  well under the timeout. Wire ready/reconnecting/end events to a
  connection-state hook.
- cache-adapter: setMetricsCallback fired on every timed-out/errored op.
- metrics: backend_redis_client_up gauge + redis_command_errors_total
  counter (named distinctly from redis_exporter's server-side redis_up).
- http-server: wire the gauge (via state hook) and the error counter.
- alert-rules: BackendRedisClientDown (gauge==0 2m) and
  BackendRedisCommandErrors (rate>0.2/s 3m) — the latter is the signal
  that catches a stuck client while it still reports "up".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcp_backend/src/utils/redis-client.ts">

<violation number="1" location="mcp_backend/src/utils/redis-client.ts:58">
P3: The Redis client unit test is now stale: it still expects a `disconnect` handler, while this code registers `ready`, `reconnecting`, and `end`. Updating `src/utils/__tests__/redis-client.test.ts` to assert the new events would keep CI aligned with the intended node-redis event model.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


redisClient.on('disconnect', () => {
logger.warn('[Redis] Disconnected');
redisClient.on('ready', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Redis client unit test is now stale: it still expects a disconnect handler, while this code registers ready, reconnecting, and end. Updating src/utils/__tests__/redis-client.test.ts to assert the new events would keep CI aligned with the intended node-redis event model.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcp_backend/src/utils/redis-client.ts, line 58:

<comment>The Redis client unit test is now stale: it still expects a `disconnect` handler, while this code registers `ready`, `reconnecting`, and `end`. Updating `src/utils/__tests__/redis-client.test.ts` to assert the new events would keep CI aligned with the intended node-redis event model.</comment>

<file context>
@@ -36,11 +55,24 @@ export async function getRedisClient(): Promise<ReturnType<typeof createClient>
 
-    redisClient.on('disconnect', () => {
-      logger.warn('[Redis] Disconnected');
+    redisClient.on('ready', () => {
+      stateHook?.(true);
+    });
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant