From e45e140b411cf0d831a615c7c7bb728a79172dfa Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 10 Oct 2025 00:57:28 +0700 Subject: [PATCH] Fix caching and loop query on the get current user --- SQL_MONITORING_README.md | 1 + .../Controllers/UserController.cs | 1 - src/Managing.Application/Users/UserService.cs | 4 +- .../PostgreSql/SqlLoopDetectionService.cs | 30 +++-- .../CacheService.cs | 6 +- .../src/app/store/userStore.tsx | 105 ++++++++++++++++++ .../src/pages/botsPage/botList.tsx | 25 +---- .../src/pages/botsPage/bots.tsx | 24 ++-- .../pages/settingsPage/UserInfoSettings.tsx | 8 +- 9 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 src/Managing.WebApp/src/app/store/userStore.tsx diff --git a/SQL_MONITORING_README.md b/SQL_MONITORING_README.md index f92424f6..560b5c5f 100644 --- a/SQL_MONITORING_README.md +++ b/SQL_MONITORING_README.md @@ -334,3 +334,4 @@ This comprehensive SQL monitoring system provides the tools needed to identify a - **Configurable settings** for different environments The system is designed to be non-intrusive while providing maximum visibility into database operations, helping you quickly identify and resolve performance issues and potential infinite loops. + diff --git a/src/Managing.Api/Controllers/UserController.cs b/src/Managing.Api/Controllers/UserController.cs index ca55f71d..bb26d070 100644 --- a/src/Managing.Api/Controllers/UserController.cs +++ b/src/Managing.Api/Controllers/UserController.cs @@ -64,7 +64,6 @@ public class UserController : BaseController public async Task> GetCurrentUser() { var user = await base.GetUser(); - user = await _userService.GetUserByName(user.Name); return Ok(user); } diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index 21271250..23618175 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -149,10 +149,10 @@ public class UserService : IUserService // Use proper async version to avoid DbContext concurrency issues user.Accounts = (await _accountService.GetAccountsByUserAsync(user)).ToList(); - // Save to cache for 10 minutes if caching is enabled (JWT middleware calls this on every request) + // Save to cache for 5 minutes if caching is enabled (JWT middleware calls this on every request) if (useCache) { - _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10)); + _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5)); } return user; diff --git a/src/Managing.Infrastructure.Database/PostgreSql/SqlLoopDetectionService.cs b/src/Managing.Infrastructure.Database/PostgreSql/SqlLoopDetectionService.cs index 8543791f..43967b00 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/SqlLoopDetectionService.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/SqlLoopDetectionService.cs @@ -20,7 +20,7 @@ public class SqlLoopDetectionService { _logger = logger; _queryTrackers = new ConcurrentDictionary(); - + // Setup cleanup timer to remove old tracking data _cleanupTimer = new Timer(CleanupOldTrackers, null, _cleanupInterval, _cleanupInterval); } @@ -33,12 +33,13 @@ public class SqlLoopDetectionService /// Pattern or hash of the query being executed /// Time taken to execute the query /// True if a potential loop is detected - public bool TrackQueryExecution(string repositoryName, string methodName, string queryPattern, TimeSpan executionTime) + public bool TrackQueryExecution(string repositoryName, string methodName, string queryPattern, + TimeSpan executionTime) { var key = $"{repositoryName}.{methodName}.{queryPattern}"; var now = DateTime.UtcNow; - - var tracker = _queryTrackers.AddOrUpdate(key, + + var tracker = _queryTrackers.AddOrUpdate(key, new QueryExecutionTracker { RepositoryName = repositoryName, @@ -56,8 +57,12 @@ public class SqlLoopDetectionService existing.LastExecution = now; existing.ExecutionCount++; existing.TotalExecutionTime += executionTime; - existing.MaxExecutionTime = existing.MaxExecutionTime > executionTime ? existing.MaxExecutionTime : executionTime; - existing.MinExecutionTime = existing.MinExecutionTime < executionTime ? existing.MinExecutionTime : executionTime; + existing.MaxExecutionTime = existing.MaxExecutionTime > executionTime + ? existing.MaxExecutionTime + : executionTime; + existing.MinExecutionTime = existing.MinExecutionTime < executionTime + ? existing.MinExecutionTime + : executionTime; return existing; }); @@ -86,7 +91,8 @@ public class SqlLoopDetectionService if (tracker.ExecutionCount > 5 && timeSinceFirst.TotalSeconds < 10) { isLoopDetected = true; - reasons.Add($"Rapid execution: {tracker.ExecutionCount} executions in {timeSinceFirst.TotalSeconds:F1} seconds"); + reasons.Add( + $"Rapid execution: {tracker.ExecutionCount} executions in {timeSinceFirst.TotalSeconds:F1} seconds"); } // Check for consistently slow queries @@ -100,13 +106,13 @@ public class SqlLoopDetectionService { _logger.LogWarning( "[SQL-LOOP-DETECTED] {Repository}.{Method} | Pattern: {Pattern} | Count: {Count} | Reasons: {Reasons} | Avg Time: {AvgTime}ms", - repositoryName, methodName, queryPattern, tracker.ExecutionCount, + repositoryName, methodName, queryPattern, tracker.ExecutionCount, string.Join(", ", reasons), tracker.AverageExecutionTime.TotalMilliseconds); // Log detailed execution history _logger.LogWarning( "[SQL-LOOP-DETAILS] {Repository}.{Method} | First: {First} | Last: {Last} | Min: {Min}ms | Max: {Max}ms | Total: {Total}ms", - repositoryName, methodName, tracker.FirstExecution.ToString("HH:mm:ss.fff"), + repositoryName, methodName, tracker.FirstExecution.ToString("HH:mm:ss.fff"), tracker.LastExecution.ToString("HH:mm:ss.fff"), tracker.MinExecutionTime.TotalMilliseconds, tracker.MaxExecutionTime.TotalMilliseconds, tracker.TotalExecutionTime.TotalMilliseconds); } @@ -126,7 +132,7 @@ public class SqlLoopDetectionService { var tracker = kvp.Value; var timeSinceFirst = now - tracker.FirstExecution; - + stats[kvp.Key] = new QueryExecutionStats { RepositoryName = tracker.RepositoryName, @@ -197,7 +203,7 @@ public class SqlLoopDetectionService public TimeSpan MaxExecutionTime { get; set; } public TimeSpan MinExecutionTime { get; set; } - public TimeSpan AverageExecutionTime => + public TimeSpan AverageExecutionTime => ExecutionCount > 0 ? TimeSpan.FromTicks(TotalExecutionTime.Ticks / ExecutionCount) : TimeSpan.Zero; } } @@ -218,4 +224,4 @@ public class QueryExecutionStats public TimeSpan MaxExecutionTime { get; set; } public double ExecutionsPerMinute { get; set; } public bool IsActive { get; set; } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Storage/CacheService.cs b/src/Managing.Infrastructure.Storage/CacheService.cs index 89d6a84c..2fa92e31 100644 --- a/src/Managing.Infrastructure.Storage/CacheService.cs +++ b/src/Managing.Infrastructure.Storage/CacheService.cs @@ -45,7 +45,8 @@ namespace Managing.Infrastructure.Storage { var options = new DistributedCacheEntryOptions() { - SlidingExpiration = slidingExpiration + SlidingExpiration = slidingExpiration, + AbsoluteExpiration = DateTime.UtcNow.AddHours(1) }; distributedCache.SetString(name, JsonConvert.SerializeObject(value), options); @@ -62,7 +63,8 @@ namespace Managing.Infrastructure.Storage var options = new DistributedCacheEntryOptions() { - SlidingExpiration = slidingExpiration + SlidingExpiration = slidingExpiration, + AbsoluteExpiration = DateTime.UtcNow.AddHours(1) }; var result = action(); diff --git a/src/Managing.WebApp/src/app/store/userStore.tsx b/src/Managing.WebApp/src/app/store/userStore.tsx new file mode 100644 index 00000000..130b7cc4 --- /dev/null +++ b/src/Managing.WebApp/src/app/store/userStore.tsx @@ -0,0 +1,105 @@ +import {create} from 'zustand' +import {useQuery, useQueryClient} from '@tanstack/react-query' +import {User, UserClient} from '../../generated/ManagingApi' +import useApiUrlStore from './apiStore' +import useCookie from '../../hooks/useCookie' +// Import React for useEffect +import React from 'react' + +interface UserStore { + currentUser: User | null + isLoading: boolean + error: Error | null + setCurrentUser: (user: User | null) => void + setLoading: (loading: boolean) => void + setError: (error: Error | null) => void + clearUser: () => void +} + +export const useUserStore = create((set, get) => ({ + currentUser: null, + isLoading: false, + error: null, + setCurrentUser: (user: User | null) => set({ currentUser: user }), + setLoading: (loading: boolean) => set({ isLoading: loading }), + setError: (error: Error | null) => set({ error }), + clearUser: () => set({ currentUser: null, error: null }), +})) + +// Custom hook that integrates TanStack Query with Zustand store +export const useCurrentUser = () => { + const { apiUrl } = useApiUrlStore() + const { getCookie } = useCookie() + const queryClient = useQueryClient() + + // Get JWT token from cookies + const jwtToken = getCookie('token') + + // TanStack Query for fetching user data + const query = useQuery({ + queryKey: ['currentUser'], + queryFn: async (): Promise => { + const userClient = new UserClient({}, apiUrl) + return await userClient.user_GetCurrentUser() + }, + enabled: !!jwtToken, // Only fetch when JWT token exists + staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes + gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes + retry: 2, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }) + + // Zustand store state + const store = useUserStore() + + // Sync TanStack Query state with Zustand store + React.useEffect(() => { + if (query.data) { + store.setCurrentUser(query.data) + } + store.setLoading(query.isLoading) + store.setError(query.error as Error | null) + }, [query.data, query.isLoading, query.error, store]) + + // Return both TanStack Query data and store state for flexibility + return { + // TanStack Query data (preferred for most use cases) + user: query.data, + isLoading: query.isLoading, + error: query.error, + refetch: query.refetch, + + // Store state (for components that need it) + storeUser: store.currentUser, + storeLoading: store.isLoading, + storeError: store.error, + + // Utility functions + clearCache: () => { + queryClient.removeQueries({ queryKey: ['currentUser'] }) + store.clearUser() + }, + invalidateCache: () => { + queryClient.invalidateQueries({ queryKey: ['currentUser'] }) + }, + } +} + +// Hook for components that only need the user data without loading states +export const useCurrentUserData = () => { + const { user } = useCurrentUser() + return user +} + +// Hook for components that need to check if current user owns something +export const useIsBotOwner = (botAgentName: string) => { + const { user } = useCurrentUser() + return user?.agentName === botAgentName +} + +// Hook for getting just the agent name +export const useCurrentAgentName = () => { + const { user } = useCurrentUser() + return user?.agentName || null +} + diff --git a/src/Managing.WebApp/src/pages/botsPage/botList.tsx b/src/Managing.WebApp/src/pages/botsPage/botList.tsx index 5f3d025e..c06ac691 100644 --- a/src/Managing.WebApp/src/pages/botsPage/botList.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/botList.tsx @@ -2,6 +2,7 @@ import {ChartBarIcon, CogIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, Tras import React, {useState} from 'react' import useApiUrlStore from '../../app/store/apiStore' +import {useIsBotOwner} from '../../app/store/userStore' import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules' import ManualPositionModal from '../../components/mollecules/ManualPositionModal' import TradesModal from '../../components/mollecules/TradesModal/TradesModal' @@ -12,13 +13,10 @@ import { MoneyManagement, Position, TradingBotConfig, - TradingBotResponse, - UserClient + TradingBotResponse } from '../../generated/ManagingApi' import type {IBotList} from '../../global/type.tsx' import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal' -import {useQuery} from '@tanstack/react-query' -import useCookie from '../../hooks/useCookie' function baseBadgeClass(isOutlined = false) { let classes = 'text-xs badge badge-sm transition-all duration-200 hover:scale-105 ' @@ -53,17 +51,9 @@ function cardClasses(botStatus: BotStatus) { const BotList: React.FC = ({ list }) => { const { apiUrl } = useApiUrlStore() const client = new BotClient({}, apiUrl) - const userClient = new UserClient({}, apiUrl) - const { getCookie } = useCookie() - // Get JWT token from cookies - const jwtToken = getCookie('token') - - const { data: currentUser } = useQuery({ - queryFn: () => userClient.user_GetCurrentUser(), - queryKey: ['currentUser'], - enabled: !!jwtToken, // Only fetch when JWT token exists - }) + // Use the new user store hook for bot ownership checking + const checkIsBotOwner = useIsBotOwner const [showMoneyManagementModal, setShowMoneyManagementModal] = useState(false) @@ -79,11 +69,6 @@ const BotList: React.FC = ({ list }) => { config: TradingBotConfig } | null>(null) - // Helper function to check if current user owns the bot - const isBotOwner = (botAgentName: string): boolean => { - return currentUser?.agentName === botAgentName - } - function getDeleteBadge(identifier: string) { const classes = baseBadgeClass() + 'bg-error' return ( @@ -259,7 +244,7 @@ const BotList: React.FC = ({ list }) => {
{/* Action Badges - Only show for bot owners */} - {isBotOwner(bot.agentName) && ( + {checkIsBotOwner(bot.agentName) && (
{getToggleBotStatusBadge(bot.status as BotStatus, bot.identifier)} {getUpdateBotBadge(bot)} diff --git a/src/Managing.WebApp/src/pages/botsPage/bots.tsx b/src/Managing.WebApp/src/pages/botsPage/bots.tsx index f41e9fb8..33e43f19 100644 --- a/src/Managing.WebApp/src/pages/botsPage/bots.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/bots.tsx @@ -2,11 +2,12 @@ import {ViewGridAddIcon} from '@heroicons/react/solid' import React, {useState} from 'react' import 'react-toastify/dist/ReactToastify.css' import useApiUrlStore from '../../app/store/apiStore' +import {useCurrentAgentName} from '../../app/store/userStore' import {UnifiedTradingModal} from '../../components/organism' -import {BotClient, BotSortableColumn, BotStatus, UserClient} from '../../generated/ManagingApi' +import {BotClient, BotSortableColumn, BotStatus} from '../../generated/ManagingApi' +import {useQuery} from '@tanstack/react-query' import BotList from './botList' -import {useQuery} from '@tanstack/react-query' const Bots: React.FC = () => { const [activeTab, setActiveTab] = useState(0) @@ -21,12 +22,9 @@ const Bots: React.FC = () => { } const { apiUrl } = useApiUrlStore() const botClient = new BotClient({}, apiUrl) - const userClient = new UserClient({}, apiUrl) - - const { data: currentUser } = useQuery({ - queryFn: () => userClient.user_GetCurrentUser(), - queryKey: ['currentUser'], - }) + + // Use the new user store hook to get current agent name + const currentAgentName = useCurrentAgentName() // Query for paginated bots using the new endpoint const { data: paginatedBots } = useQuery({ @@ -35,17 +33,17 @@ const Bots: React.FC = () => { case 0: // All Active Bots return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Running, undefined, undefined, undefined, BotSortableColumn.Roi, 'Desc') case 1: // My Active Bots - return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Running, undefined, undefined, currentUser?.agentName, BotSortableColumn.Roi, 'Desc') + return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Running, undefined, undefined, currentAgentName, BotSortableColumn.Roi, 'Desc') case 2: // My Down Bots - return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Stopped, undefined, undefined, currentUser?.agentName, BotSortableColumn.Roi, 'Desc') + return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Stopped, undefined, undefined, currentAgentName, BotSortableColumn.Roi, 'Desc') case 3: // Saved Bots - return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Saved, undefined, undefined, currentUser?.agentName, BotSortableColumn.Roi, 'Desc') + return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Saved, undefined, undefined, currentAgentName, BotSortableColumn.Roi, 'Desc') default: return botClient.bot_GetBotsPaginated(pageNumber, pageSize, undefined, undefined, undefined, undefined, BotSortableColumn.Roi, 'Desc') } }, - queryKey: ['paginatedBots', activeTab, pageNumber, pageSize, currentUser?.agentName], - enabled: !!currentUser, + queryKey: ['paginatedBots', activeTab, pageNumber, pageSize, currentAgentName], + enabled: !!currentAgentName, }) const filteredBots = paginatedBots?.items || [] diff --git a/src/Managing.WebApp/src/pages/settingsPage/UserInfoSettings.tsx b/src/Managing.WebApp/src/pages/settingsPage/UserInfoSettings.tsx index 4d654995..e19fce9d 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/UserInfoSettings.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/UserInfoSettings.tsx @@ -32,7 +32,7 @@ function UserInfoSettings() { const jwtToken = getCookie('token') const { data: user } = useQuery({ - queryKey: ['user'], + queryKey: ['currentUser'], queryFn: () => api.user_GetCurrentUser(), enabled: !!jwtToken, // Only fetch when JWT token exists }) @@ -59,7 +59,7 @@ function UserInfoSettings() { const toast = new Toast('Updating agent name') try { await api.user_UpdateAgentName(data.agentName) - queryClient.invalidateQueries({ queryKey: ['user'] }) + queryClient.invalidateQueries({ queryKey: ['currentUser'] }) setShowUpdateModal(false) toast.update('success', 'Agent name updated successfully') } catch (error) { @@ -72,7 +72,7 @@ function UserInfoSettings() { const toast = new Toast('Updating avatar') try { await api.user_UpdateAvatarUrl(data.avatarUrl) - queryClient.invalidateQueries({ queryKey: ['user'] }) + queryClient.invalidateQueries({ queryKey: ['currentUser'] }) setShowAvatarModal(false) toast.update('success', 'Avatar updated successfully') } catch (error) { @@ -85,7 +85,7 @@ function UserInfoSettings() { const toast = new Toast('Updating telegram channel') try { await api.user_UpdateTelegramChannel(data.telegramChannel) - queryClient.invalidateQueries({ queryKey: ['user'] }) + queryClient.invalidateQueries({ queryKey: ['currentUser'] }) setShowTelegramModal(false) toast.update('success', 'Telegram channel updated successfully') } catch (error) {