Fix caching and loop query on the get current user
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ public class UserController : BaseController
|
||||
public async Task<ActionResult<User>> GetCurrentUser()
|
||||
{
|
||||
var user = await base.GetUser();
|
||||
user = await _userService.GetUserByName(user.Name);
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@ public class SqlLoopDetectionService
|
||||
{
|
||||
_logger = logger;
|
||||
_queryTrackers = new ConcurrentDictionary<string, QueryExecutionTracker>();
|
||||
|
||||
|
||||
// Setup cleanup timer to remove old tracking data
|
||||
_cleanupTimer = new Timer(CleanupOldTrackers, null, _cleanupInterval, _cleanupInterval);
|
||||
}
|
||||
@@ -33,12 +33,13 @@ public class SqlLoopDetectionService
|
||||
/// <param name="queryPattern">Pattern or hash of the query being executed</param>
|
||||
/// <param name="executionTime">Time taken to execute the query</param>
|
||||
/// <returns>True if a potential loop is detected</returns>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
105
src/Managing.WebApp/src/app/store/userStore.tsx
Normal file
105
src/Managing.WebApp/src/app/store/userStore.tsx
Normal file
@@ -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<UserStore>((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<User> => {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<IBotList> = ({ 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<IBotList> = ({ 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<IBotList> = ({ list }) => {
|
||||
<div className="flex flex-wrap gap-1 sm:gap-2">
|
||||
|
||||
{/* Action Badges - Only show for bot owners */}
|
||||
{isBotOwner(bot.agentName) && (
|
||||
{checkIsBotOwner(bot.agentName) && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{getToggleBotStatusBadge(bot.status as BotStatus, bot.identifier)}
|
||||
{getUpdateBotBadge(bot)}
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user