From 2367ca289e321381dcc0e4c81e591cd8e123a0af Mon Sep 17 00:00:00 2001 From: hey-Zayn Date: Sun, 29 Mar 2026 06:17:48 +0500 Subject: [PATCH 1/2] refactor: redis for 500+ users --- backend/src/Database/connection.js | 15 ++++++++++++--- backend/src/controllers/song.controller.js | 1 + backend/src/lib/cacheManager.js | 19 ++++++++++++++----- backend/src/models/song.model.js | 2 +- .../pages/admin/components/AddSongDialog.tsx | 11 ++++++----- .../pages/admin/components/EditSongDialog.tsx | 10 +++++----- 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/backend/src/Database/connection.js b/backend/src/Database/connection.js index 86cdc32..2854ea4 100644 --- a/backend/src/Database/connection.js +++ b/backend/src/Database/connection.js @@ -1,18 +1,27 @@ const mongoose = require('mongoose'); +let isConnected = false; + const connectDB = async () => { + if (isConnected) { + return; + } + try { const mongoUrl = process.env.MONGODB_URI || process.env.MONGO_URL; if (!mongoUrl) { throw new Error("MONGODB_URI or MONGO_URL not found in environment variables"); } - await mongoose.connect(mongoUrl); + await mongoose.connect(mongoUrl, { + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, + }); + + isConnected = true; console.log(`MongoDB connected successfully ${mongoose.connection.host}`); } catch (err) { console.error(`MongoDB connection failed: ${err.message}`); - // In production/serverless, we shouldn't necessarily exit the process - // instead let the error propagate or handled by the app if (process.env.NODE_ENV !== 'production') { process.exit(1); } diff --git a/backend/src/controllers/song.controller.js b/backend/src/controllers/song.controller.js index 286067f..40016b6 100644 --- a/backend/src/controllers/song.controller.js +++ b/backend/src/controllers/song.controller.js @@ -138,6 +138,7 @@ const createSong = async (req, res, next) => { if (!title) return res.status(400).json({ success: false, message: "Title is required" }); if (!artist) return res.status(400).json({ success: false, message: "Artist is required" }); if (!duration) return res.status(400).json({ success: false, message: "Duration is required" }); + if (!albumId || albumId === "none") return res.status(400).json({ success: false, message: "Album is required" }); const audioUrl = await uploadToCloudinary(req.files.audioFile); const imageUrl = await uploadToCloudinary(req.files.imageFile); diff --git a/backend/src/lib/cacheManager.js b/backend/src/lib/cacheManager.js index 3616f3f..293b15a 100644 --- a/backend/src/lib/cacheManager.js +++ b/backend/src/lib/cacheManager.js @@ -31,14 +31,23 @@ const CacheManager = { }, /** - * Purges keys using wildcards (Note: keys() is O(N), use sparingly) + * Purges keys using non-blocking SCAN (O(1) per step) */ async purgePattern(pattern) { try { - const keys = await redisClient.keys(pattern); - if (keys.length > 0) { - await redisClient.del(keys); - console.log(`Purged ${keys.length} keys matching ${pattern}`); + let totalPurged = 0; + const iterator = await redisClient.scanIterator({ + MATCH: pattern, + COUNT: 100 + }); + + for await (const key of iterator) { + await redisClient.del(key); + totalPurged++; + } + + if (totalPurged > 0) { + console.log(`Purged ${totalPurged} keys matching ${pattern}`); } } catch (err) { console.error(`Cache Purge Error [${pattern}]:`, err); diff --git a/backend/src/models/song.model.js b/backend/src/models/song.model.js index 8e94c62..46f318d 100644 --- a/backend/src/models/song.model.js +++ b/backend/src/models/song.model.js @@ -24,7 +24,7 @@ const songSchema = new mongoose.Schema({ albumId: { type: mongoose.Schema.Types.ObjectId, ref: 'Album', - required: false, + required: true, }, creator: { type: String, // clerkId diff --git a/frontend/src/pages/admin/components/AddSongDialog.tsx b/frontend/src/pages/admin/components/AddSongDialog.tsx index 791fe79..928c351 100644 --- a/frontend/src/pages/admin/components/AddSongDialog.tsx +++ b/frontend/src/pages/admin/components/AddSongDialog.tsx @@ -150,14 +150,16 @@ const AddSongDialog = () => { return toast.error("Artist is required"); } + if (!newSong.album || newSong.album === "none") { + return toast.error("Please select an album"); + } + const formData = new FormData(); formData.append("title", newSong.title); formData.append("artist", newSong.artist); formData.append("duration", newSong.duration); - if (newSong.album && newSong.album !== "none") { - formData.append("albumId", newSong.album); - } + formData.append("albumId", newSong.album); formData.append("audioFile", files.audio); formData.append("imageFile", files.image); @@ -354,7 +356,7 @@ const AddSongDialog = () => {
- + setFormData({ ...formData, album: value })} @@ -147,7 +148,6 @@ const EditSongDialog = ({ song }: EditSongDialogProps) => { - No Album {albums.map((album) => ( {album.title} From 86f197ad27f6474d79537914e1613f303204b757 Mon Sep 17 00:00:00 2001 From: hey-Zayn Date: Sun, 29 Mar 2026 06:20:07 +0500 Subject: [PATCH 2/2] test: align cache manager tests with non-blocking scan logic --- backend/src/__tests__/cacheManager.test.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/backend/src/__tests__/cacheManager.test.js b/backend/src/__tests__/cacheManager.test.js index 4762e1c..1581c6a 100644 --- a/backend/src/__tests__/cacheManager.test.js +++ b/backend/src/__tests__/cacheManager.test.js @@ -47,15 +47,27 @@ describe('CacheManager', () => { }); describe('purgePattern', () => { - it('should delete keys matching a pattern', async () => { + it('should delete keys matching a pattern using scanIterator', async () => { const mockKeys = ['key1', 'key2']; - const spyKeys = vi.spyOn(redisClient, 'keys').mockResolvedValue(mockKeys); - const spyDel = vi.spyOn(redisClient, 'del').mockResolvedValue(2); + + // Mocking the async iterator for scanIterator + const spyScan = vi.spyOn(redisClient, 'scanIterator').mockReturnValue((async function* () { + for (const key of mockKeys) { + yield key; + } + })()); + + const spyDel = vi.spyOn(redisClient, 'del').mockResolvedValue(1); await CacheManager.purgePattern('pattern:*'); - expect(spyKeys).toHaveBeenCalledWith('pattern:*'); - expect(spyDel).toHaveBeenCalledWith(mockKeys); + expect(spyScan).toHaveBeenCalledWith({ + MATCH: 'pattern:*', + COUNT: 100 + }); + expect(spyDel).toHaveBeenCalledTimes(2); + expect(spyDel).toHaveBeenCalledWith('key1'); + expect(spyDel).toHaveBeenCalledWith('key2'); }); }); });