From 0749a55deb26dbd9417f8b8e0fa1ed4235550a92 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:04:31 +0200 Subject: [PATCH] POC: Add LastSeenManager to batch user activity updates --- server/Server.js | 26 ++++++- server/SocketAuthority.js | 8 +- server/managers/LastSeenManager.js | 114 +++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 server/managers/LastSeenManager.js diff --git a/server/Server.js b/server/Server.js index 22a53a3a..5c3f76e9 100644 --- a/server/Server.js +++ b/server/Server.js @@ -37,6 +37,7 @@ const CronManager = require('./managers/CronManager') const ApiCacheManager = require('./managers/ApiCacheManager') const BinaryManager = require('./managers/BinaryManager') const ShareManager = require('./managers/ShareManager') +const LastSeenManager = require('./managers/LastSeenManager') const LibraryScanner = require('./scanner/LibraryScanner') //Import the main Passport and Express-Session library @@ -107,6 +108,7 @@ class Server { this.cronManager = new CronManager(this.podcastManager, this.playbackSessionManager) this.apiCacheManager = new ApiCacheManager() this.binaryManager = new BinaryManager() + this.lastSeenManager = new LastSeenManager() // Routers this.apiRouter = new ApiRouter(this) @@ -130,6 +132,20 @@ class Server { this.auth.isAuthenticated(req, res, next) } + /** + * Middleware to track user activity for lastSeen updates + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ + lastSeenMiddleware(req, res, next) { + if (req.user && req.user.id) { + this.lastSeenManager.addActiveUser(req.user.id) + } + next() + } + cancelLibraryScan(libraryId) { LibraryScanner.setCancelLibraryScan(libraryId) } @@ -174,6 +190,7 @@ class Server { const libraries = await Database.libraryModel.getAllWithFolders() await this.cronManager.init(libraries) this.apiCacheManager.init() + this.lastSeenManager.init() if (Database.serverSettings.scannerDisableWatcher) { Logger.info(`[Server] Watcher is disabled`) @@ -311,7 +328,7 @@ class Server { router.use(express.urlencoded({ extended: true, limit: '5mb' })) router.use(express.json({ limit: '10mb' })) - router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router) + router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.lastSeenMiddleware.bind(this), this.apiRouter.router) router.use('/hls', this.hlsRouter.router) router.use('/public', this.publicRouter.router) @@ -499,6 +516,13 @@ class Server { */ async stop() { Logger.info('=== Stopping Server ===') + + // Cleanup LastSeenManager first to flush any pending updates + if (this.lastSeenManager) { + await this.lastSeenManager.cleanup() + Logger.info('[Server] LastSeenManager Cleaned Up') + } + Watcher.close() Logger.info('[Server] Watcher Closed') await SocketAuthority.close() diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 050e7e2f..13d22ea6 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -269,9 +269,11 @@ class SocketAuthority { this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions)) - // Update user lastSeen without firing sequelize bulk update hooks - user.lastSeen = Date.now() - await user.save({ hooks: false }) + if (this.Server.lastSeenManager) { + this.Server.lastSeenManager.addActiveUser(user.id) + // To ensure actual lastSeen behaviour does not change, we just flush when connecting. This should not add much overhead as this is only for the authenticated socket. + this.Server.lastSeenManager.flushActiveUsers() + } const initialPayload = { userId: client.user.id, diff --git a/server/managers/LastSeenManager.js b/server/managers/LastSeenManager.js new file mode 100644 index 00000000..0b9e6029 --- /dev/null +++ b/server/managers/LastSeenManager.js @@ -0,0 +1,114 @@ +const Logger = require('../Logger') +const Database = require('../Database') + +/** + * Manager for handling lastSeen updates + */ +class LastSeenManager { + constructor() { + /** @type {Set} Set of user IDs that have made requests */ + this.activeUsers = new Set() + + /** @type {NodeJS.Timeout} Flush interval timer */ + this.flushInterval = null + + /** @type {number} Flush interval */ + this.flushIntervalMs = 1000 * 60 * 5 + } + + /** + * Initialize the LastSeenManager + * Start the periodic flush process + */ + init() { + Logger.info('[LastSeenManager] Initializing') + this.startFlushInterval() + } + + /** + * Start the periodic flush interval + */ + startFlushInterval() { + if (this.flushInterval) { + clearInterval(this.flushInterval) + } + + this.flushInterval = setInterval(() => { + this.flushActiveUsers() + }, this.flushIntervalMs) + + Logger.info(`[LastSeenManager] Started flush interval every ${this.flushIntervalMs / 1000} seconds`) + } + + /** + * Stop the flush interval + */ + stopFlushInterval() { + if (this.flushInterval) { + clearInterval(this.flushInterval) + this.flushInterval = null + Logger.info('[LastSeenManager] Stopped flush interval') + } + } + + /** + * Add a user to the active users set + * @param {string} userId - User ID + */ + addActiveUser(userId) { + if (userId) { + this.activeUsers.add(userId) + } + } + + /** + * Flush all active users to the database and clear the set + * Updates lastSeen timestamp for all users in the set + */ + async flushActiveUsers() { + if (this.activeUsers.size === 0) { + Logger.debug('[LastSeenManager] No active users to flush') + return + } + + const userIds = Array.from(this.activeUsers) + const currentTime = Date.now() + + Logger.debug(`[LastSeenManager] Flushing ${userIds.length} active users to database`) + + try { + const affectedRows = await Database.userModel.update( + { lastSeen: new Date(currentTime) }, + { + where: { + id: userIds + }, + hooks: false + } + ) + + Logger.debug(`[LastSeenManager] Successfully updated lastSeen for ${affectedRows[0]} users`) + + // Clear the active users set + this.activeUsers.clear() + } catch (error) { + Logger.error(`[LastSeenManager] Failed to flush active users:`, error) + } + } + + async forceFlush() { + Logger.info('[LastSeenManager] Force flushing active users') + await this.flushActiveUsers() + } + + /** + * Cleanup and stop all processes + */ + async cleanup() { + Logger.info('[LastSeenManager] Cleaning up') + this.stopFlushInterval() + await this.forceFlush() + } +} + +module.exports = LastSeenManager