Skip to content

(SP: 1) [Backend] Move session heartbeat from DB writes to Redis sorted set#411

Merged
ViktorSvertoka merged 1 commit intodevelopfrom
sl/feat/db-optimization
Mar 22, 2026
Merged

(SP: 1) [Backend] Move session heartbeat from DB writes to Redis sorted set#411
ViktorSvertoka merged 1 commit intodevelopfrom
sl/feat/db-optimization

Conversation

@LesiaUKR
Copy link
Collaborator

@LesiaUKR LesiaUKR commented Mar 22, 2026

Summary

  • Session heartbeat (/api/sessions/activity) moved from PostgreSQL writes to Redis sorted set pipeline (ZADD + ZREMRANGEBYSCORE + ZCARD = 1 HTTP call)
  • DB fallback preserved — if Redis unavailable, existing DB logic runs as before
  • SESSION_TIMEOUT_MS constant replaces 2 duplicate inline calculations

Test plan

  • npm run build passes
  • Online counter shows correct number on homepage
  • Verified in Upstash CLI: ZRANGE online_sessions 0 -1 WITHSCORES returns session with updated scores
  • Remove Redis env vars → verify fallback to DB works (counter still shows)
  • Monitor Neon dashboard for reduced write operations after deploy

Closes #410

Acknowledged. Upstash managed Redis has high availability, and the counter
self-heals within 60s as active users re-heartbeat. Adding outage tracking
would require persistent state across serverless instances — more complexity
than the edge case warrants. Will revisit if Redis outages become a real issue

Summary by CodeRabbit

Bug Fixes

  • Significantly enhanced reliability and performance of online session tracking. The system now more accurately maintains real-time active user counts with optimized mechanisms and improved fallback handling to ensure consistent service delivery, even when primary systems experience temporary unavailability.

Chores

  • Updated platform statistics default values for improved accuracy and consistency.

fix(about): update LinkedIn follower fallback to 1800
@vercel
Copy link
Contributor

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
devlovers-net Ignored Ignored Mar 22, 2026 3:31pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

This PR implements a Redis-based session heartbeat mechanism for the /api/sessions/activity endpoint, replacing direct database writes with a Redis sorted set approach. The handler now attempts Redis operations first, falling back to database queries if Redis is unavailable. A minor LinkedIn statistics default value was also updated.

Changes

Cohort / File(s) Summary
Session Heartbeat Redis Migration
frontend/app/api/sessions/activity/route.ts
Introduced heartbeatViaRedis() helper using Redis sorted set with pipelined ZADD, ZREMRANGEBYSCORE, and ZCARD commands. Extracted existing DB logic into heartbeatViaDb() fallback. Handler now attempts Redis first via nullish coalescing; added SESSION_TIMEOUT_MS constant for timeout calculations in both paths. Wrapped POST logic in try/catch for error handling.
Statistics Default Update
frontend/lib/about/stats.ts
Updated default LinkedIn follower count fallback from 1700 to 1800 when environment variable is unset.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as POST Handler
    participant Redis as Redis Client
    participant DB as Database

    Client->>Handler: POST /api/sessions/activity
    Handler->>Redis: heartbeatViaRedis(sessionId)
    alt Redis Available
        Redis->>Redis: ZADD + ZREMRANGEBYSCORE + ZCARD (pipeline)
        Redis-->>Handler: online count
    else Redis Unavailable/Fails
        Redis-->>Handler: null
        Handler->>DB: heartbeatViaDb(sessionId)
        DB->>DB: UPSERT activeSessions
        DB->>DB: COUNT sessions (timeout check)
        DB->>DB: Cleanup stale sessions
        DB-->>Handler: online count
    end
    Handler-->>Client: { online: count }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 Redis hops in, quick and spry,
Sessions dance through cache so high,
Database waits when Redis glows,
Heartbeats flow where fast stream goes! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes an unrelated change to frontend/lib/about/stats.ts (updating LinkedIn follower fallback from 1700 to 1800), which is out of scope for the session heartbeat Redis migration objective. Remove the unrelated LinkedIn follower count change (frontend/lib/about/stats.ts) from this PR; commit it separately as it is not part of the session heartbeat migration scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly and clearly summarizes the main change: moving session heartbeat from database writes to a Redis sorted set implementation.
Linked Issues check ✅ Passed The implementation addresses all primary coding requirements from issue #410: heartbeatViaRedis() with ZADD/ZREMRANGEBYSCORE/ZCARD pipeline, heartbeatViaDb() fallback, Redis-first pattern with nullish coalescing, SESSION_TIMEOUT_MS constant, and zero DB writes during normal operation.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sl/feat/db-optimization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.

Change the reviews.profile setting to assertive to make CodeRabbit's nitpick more issues in your PRs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/app/api/sessions/activity/route.ts (1)

57-62: Consider moving fallback-table cleanup off the request path.

Now that steady-state traffic stays on Redis, this delete only runs during outages. That means stale active_sessions rows from fallback periods can sit around indefinitely between incidents. A small scheduled cleanup job would keep the fallback table bounded without reintroducing hot-path DB work.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/api/sessions/activity/route.ts` around lines 57 - 62, Remove the
ad-hoc probabilistic cleanup from the request handler and instead implement a
scheduled/background cleanup that runs off the request path; specifically,
delete the Math.random() block that calls
db.delete(activeSessions).where(lt(activeSessions.lastActivity,
cleanupThreshold)), and create a new periodic job (e.g.,
cleanupActiveSessionsJob or a cron/scheduled function) that computes
cleanupThreshold using SESSION_TIMEOUT_MS and performs
db.delete(activeSessions).where(lt(activeSessions.lastActivity,
cleanupThreshold)) with proper error handling and logging; ensure the route no
longer performs this DB work so fallback-table maintenance is bounded by the
independent scheduled task.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/api/sessions/activity/route.ts`:
- Around line 83-84: The current logic flips back to Redis immediately when
heartbeatViaRedis(sessionId) returns a value, causing undercount after transient
Redis outages; change the logic so that when heartbeatViaRedis(sessionId)
returns null/undefined (indicating an outage) you record the outage time and use
heartbeatViaDb(sessionId) as the source of truth for at least one timeout window
(e.g., SESSION_TIMEOUT_MS) after that outage, or alternatively seed Redis from
recent active_sessions before switching; update the code around redisCount,
online, and the functions heartbeatViaRedis/heartbeatViaDb to check a
lastRedisOutage timestamp (or a boolean flag) and continue using DB results
until the grace period expires, then resume using Redis normally.

---

Nitpick comments:
In `@frontend/app/api/sessions/activity/route.ts`:
- Around line 57-62: Remove the ad-hoc probabilistic cleanup from the request
handler and instead implement a scheduled/background cleanup that runs off the
request path; specifically, delete the Math.random() block that calls
db.delete(activeSessions).where(lt(activeSessions.lastActivity,
cleanupThreshold)), and create a new periodic job (e.g.,
cleanupActiveSessionsJob or a cron/scheduled function) that computes
cleanupThreshold using SESSION_TIMEOUT_MS and performs
db.delete(activeSessions).where(lt(activeSessions.lastActivity,
cleanupThreshold)) with proper error handling and logging; ensure the route no
longer performs this DB work so fallback-table maintenance is bounded by the
independent scheduled task.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 54041997-e70a-4989-b5bc-ad458d23d191

📥 Commits

Reviewing files that changed from the base of the PR and between 156094e and 9545943.

📒 Files selected for processing (2)
  • frontend/app/api/sessions/activity/route.ts
  • frontend/lib/about/stats.ts

Comment on lines +83 to +84
const redisCount = await heartbeatViaRedis(sessionId);
const online = redisCount ?? (await heartbeatViaDb(sessionId));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid switching back to Redis count immediately after an outage.

Sessions written via heartbeatViaDb() exist only in active_sessions, so redisCount ?? ... undercounts as soon as Redis starts responding again. After a transient outage, the online number can collapse until every still-active user heartbeats back into Redis. Consider keeping DB as the source of truth for one timeout window after a Redis failure, or seeding Redis from recent active_sessions rows before switching back.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/api/sessions/activity/route.ts` around lines 83 - 84, The
current logic flips back to Redis immediately when heartbeatViaRedis(sessionId)
returns a value, causing undercount after transient Redis outages; change the
logic so that when heartbeatViaRedis(sessionId) returns null/undefined
(indicating an outage) you record the outage time and use
heartbeatViaDb(sessionId) as the source of truth for at least one timeout window
(e.g., SESSION_TIMEOUT_MS) after that outage, or alternatively seed Redis from
recent active_sessions before switching; update the code around redisCount,
online, and the functions heartbeatViaRedis/heartbeatViaDb to check a
lastRedisOutage timestamp (or a boolean flag) and continue using DB results
until the grace period expires, then resume using Redis normally.

@ViktorSvertoka ViktorSvertoka merged commit c4549a5 into develop Mar 22, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the sl/feat/db-optimization branch March 22, 2026 15:45
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.

2 participants