fix: 修复后端加载阻塞和NaN序列化问题
## 修复的问题 1. **数据更新服务阻塞事件循环** - 临时禁用 data_updater 服务,避免应用启动时阻塞 - 添加 TODO 标记,需要重构为真正的异步操作 2. **NaN 值无法JSON序列化** - 新增 json_utils.py 工具函数处理NaN、Infinity值 - 应用到 smart_cache 历史数据接口 - 将NaN值转换为null,避免JSON序列化错误 ## 影响的文件 - backend/main.py: 注释掉 data_updater 启动代码 - backend/app/routes/smart_cache.py: 添加 sanitize_response 处理 - backend/app/utils/json_utils.py: 新增NaN清理工具函数 - BACKEND_LOADING_ISSUES_FIX.md: 详细记录问题和修复方案 ## 验证结果 ✅ Health check API 正常响应 ✅ Strategies API 返回数据 ✅ Risk events API 返回数据 ✅ Historical data API 不再抛出NaN错误 🔧 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# Backend Loading Issues - Root Cause Analysis & Fixes
|
||||
|
||||
**Date**: 2025-12-25
|
||||
**Status**: ✅ Fixed
|
||||
|
||||
---
|
||||
|
||||
## Problem Summary
|
||||
|
||||
User reported that after page refresh, the dashboard components were stuck in loading state indefinitely:
|
||||
- "今日推荐" (Today's Recommendations)
|
||||
- "主题 & 策略" (Themes & Strategies)
|
||||
- "当前风险事件" (Current Risk Events)
|
||||
- "未来风险日历" (Future Risk Calendar)
|
||||
|
||||
## Root Causes Identified
|
||||
|
||||
### Issue 1: Data Updater Service Blocking Event Loop
|
||||
|
||||
**File**: `/backend/main.py` (lines 49-56)
|
||||
|
||||
**Problem**:
|
||||
- The `data_updater_service` was executing synchronous data fetching operations during application startup
|
||||
- This completely blocked the FastAPI event loop, preventing all API requests from being processed
|
||||
- Even health check endpoints were timing out
|
||||
|
||||
**Technical Details**:
|
||||
```python
|
||||
# Original code (BLOCKING)
|
||||
data_updater = get_data_updater()
|
||||
await data_updater.start() # This blocks for 60+ seconds
|
||||
```
|
||||
|
||||
The data updater performs these operations synchronously:
|
||||
- Fetches real-time quotes for 30+ stocks via TuShare
|
||||
- Downloads 3-month historical data on first load
|
||||
- Calculates technical indicators
|
||||
- All these operations block the async event loop
|
||||
|
||||
**Symptoms**:
|
||||
- All API endpoints hung indefinitely
|
||||
- Backend process showed "online" in PM2 but was unresponsive
|
||||
- Network connections established but no responses sent
|
||||
- Frontend showed perpetual loading states
|
||||
|
||||
**Fix Applied**:
|
||||
Temporarily disabled the data_updater service (lines 49-56 in `main.py`):
|
||||
```python
|
||||
# Start data updater service (temporarily disabled to avoid blocking)
|
||||
# TODO: Fix data_updater to use truly async operations
|
||||
try:
|
||||
# data_updater = get_data_updater()
|
||||
# await data_updater.start()
|
||||
logger.info("⚠️ Data updater service temporarily disabled (blocking issue)")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Data updater service start warning: {e}")
|
||||
```
|
||||
|
||||
**Long-term Fix Needed**:
|
||||
The data_updater service needs refactoring to use truly async operations:
|
||||
1. Replace synchronous HTTP calls with `aiohttp`
|
||||
2. Or run blocking operations in a thread pool executor
|
||||
3. Or defer all data loading until after application startup completes
|
||||
|
||||
---
|
||||
|
||||
### Issue 2: NaN Values in JSON Responses
|
||||
|
||||
**File**: `/backend/app/routes/smart_cache.py` (line 428)
|
||||
|
||||
**Problem**:
|
||||
- Pandas DataFrames converted to dictionaries contained NaN (Not a Number) float values
|
||||
- Python's `json.dumps()` cannot serialize NaN, Infinity, or -Infinity values
|
||||
- This caused HTTP 500 errors with: `ValueError: Out of range float values are not JSON compliant: nan`
|
||||
|
||||
**Technical Details**:
|
||||
```python
|
||||
# Original code (CAN FAIL)
|
||||
data = df.to_dict('records') # May contain NaN values
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"data": data, # Fails JSON serialization if data contains NaN
|
||||
"count": len(data)
|
||||
}
|
||||
```
|
||||
|
||||
**Error Example**:
|
||||
```
|
||||
2025-12-25 20:00:31 - ValueError: Out of range float values are not JSON compliant: nan
|
||||
INFO: 192.168.1.151:50600 - "GET /api/smart-cache/market/historical/601398" 500 Internal Server Error
|
||||
```
|
||||
|
||||
**Fix Applied**:
|
||||
|
||||
1. Created utility function (new file `/backend/app/utils/json_utils.py`):
|
||||
```python
|
||||
def sanitize_for_json(data: Any) -> Any:
|
||||
"""
|
||||
Recursively sanitize data to make it JSON-serializable.
|
||||
Converts NaN, Infinity, and -Infinity to None.
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return {key: sanitize_for_json(value) for key, value in data.items()}
|
||||
elif isinstance(data, (list, tuple)):
|
||||
return [sanitize_for_json(item) for item in data]
|
||||
elif isinstance(data, float):
|
||||
if math.isnan(data) or math.isinf(data):
|
||||
return None
|
||||
return data
|
||||
elif pd.isna(data):
|
||||
return None
|
||||
return data
|
||||
```
|
||||
|
||||
2. Applied to smart_cache.py endpoint:
|
||||
```python
|
||||
from app.utils.json_utils import sanitize_response
|
||||
|
||||
# In get_historical_data endpoint
|
||||
data = df.to_dict('records')
|
||||
response = {
|
||||
"symbol": symbol,
|
||||
"data": data,
|
||||
"count": len(data),
|
||||
"period": period
|
||||
}
|
||||
return sanitize_response(response) # Cleans NaN values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
### Before Fix:
|
||||
```bash
|
||||
$ curl http://localhost:8028/api/strategies/list
|
||||
# Timeout (no response)
|
||||
|
||||
$ curl http://localhost:8028/api/risk/events
|
||||
# Timeout (no response)
|
||||
|
||||
$ curl http://localhost:8028/health
|
||||
# Timeout (no response)
|
||||
```
|
||||
|
||||
### After Fix:
|
||||
```bash
|
||||
$ curl http://localhost:8028/health
|
||||
{"status":"healthy"}
|
||||
|
||||
$ curl http://localhost:8028/api/strategies/list
|
||||
{"strategies":[...]} # ✅ 4 strategies returned
|
||||
|
||||
$ curl http://localhost:8028/api/risk/events
|
||||
{"events":[...]} # ✅ 6 events returned
|
||||
|
||||
$ curl http://localhost:8028/api/risk/overview
|
||||
{"risk_level":"medium",...} # ✅ Data returned
|
||||
|
||||
$ pm2 list
|
||||
┌────┬─────────────────────┬─────────┬────────┬──────────┐
|
||||
│ id │ name │ mode │ pid │ status │
|
||||
├────┼─────────────────────┼─────────┼────────┼──────────┤
|
||||
│ 1 │ ljwx-stock-backend │ fork │ 24567 │ online │
|
||||
│ 3 │ ljwx-stock-frontend │ cluster │ 42681 │ online │
|
||||
└────┴─────────────────────┴─────────┴────────┴──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
### ✅ Working APIs:
|
||||
- `/health` - Health check (instant response)
|
||||
- `/api/strategies/list` - Strategy list (< 200ms)
|
||||
- `/api/risk/events` - Risk events (< 200ms)
|
||||
- `/api/risk/overview` - Risk overview (< 200ms)
|
||||
- `/api/smart-cache/market/historical/{symbol}` - Historical data (no NaN errors)
|
||||
|
||||
### ⚠️ Slow APIs (due to disabled data_updater):
|
||||
- `/api/recommendations` - Takes 10-30s on first request (cold data)
|
||||
- `/api/ai/recommendations/explained` - Takes 10-30s (needs real-time data fetching)
|
||||
|
||||
**Note**: These slow APIs will perform normally once the data_updater service is properly fixed and re-enabled. The slowness is expected because there's no pre-loaded/cached stock data.
|
||||
|
||||
### 🔧 Pending Work:
|
||||
- [ ] Refactor `data_updater_service.py` to use async HTTP operations
|
||||
- [ ] Re-enable data updater after fixing blocking issues
|
||||
- [ ] Consider adding circuit breakers for slow data fetching operations
|
||||
- [ ] Add loading indicators with timeout fallbacks in frontend
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **`/backend/main.py`** (lines 49-56)
|
||||
- Disabled data_updater service startup
|
||||
- Added TODO comment for future fix
|
||||
|
||||
2. **`/backend/app/utils/json_utils.py`** (NEW FILE)
|
||||
- Created NaN sanitization utility
|
||||
- Handles DataFrame, Series, and nested data structures
|
||||
|
||||
3. **`/backend/app/routes/smart_cache.py`** (lines 6-15, 428-439)
|
||||
- Added import for sanitize_response
|
||||
- Applied sanitization to historical data endpoint
|
||||
|
||||
---
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
# Test backend health
|
||||
curl http://localhost:8028/health
|
||||
|
||||
# Test strategies (for "主题 & 策略")
|
||||
curl http://localhost:8028/api/strategies/list
|
||||
|
||||
# Test risk events (for "当前风险事件")
|
||||
curl http://localhost:8028/api/risk/events
|
||||
|
||||
# Test risk overview
|
||||
curl http://localhost:8028/api/risk/overview
|
||||
|
||||
# Test historical data (previously failed with NaN error)
|
||||
curl "http://localhost:8028/api/smart-cache/market/historical/601398?start_date=20251125&end_date=20251225&period=1d"
|
||||
|
||||
# Check PM2 service status
|
||||
pm2 list
|
||||
pm2 logs ljwx-stock-backend --lines 20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `API_CONNECTION_FIX.md` - Frontend API connection and CORS configuration
|
||||
- `FRONTEND_CRASH_ANALYSIS.md` - Frontend crash issues and production build setup
|
||||
- `CURRENT_STATUS.md` - System overview and configuration
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-25 20:10
|
||||
**Status**: ✅ Major issues fixed, pending data_updater refactoring
|
||||
@@ -12,6 +12,7 @@ from pydantic import BaseModel
|
||||
from app.modules.hot_stock_detector import get_hot_stock_detector
|
||||
from app.modules.smart_prewarmer import get_smart_prewarmer
|
||||
from app.modules.real_market_fetcher import get_real_market_fetcher
|
||||
from app.utils.json_utils import sanitize_response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -424,16 +425,19 @@ async def get_historical_data(
|
||||
if df is None or df.empty:
|
||||
raise HTTPException(status_code=404, detail=f"No data found for {symbol}")
|
||||
|
||||
# 转换为字典列表
|
||||
# 转换为字典列表并清理NaN值
|
||||
data = df.to_dict('records')
|
||||
|
||||
return {
|
||||
response = {
|
||||
"symbol": symbol,
|
||||
"data": data,
|
||||
"count": len(data),
|
||||
"period": period
|
||||
}
|
||||
|
||||
# 清理NaN/Infinity值以确保JSON序列化成功
|
||||
return sanitize_response(response)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
JSON Utility Functions
|
||||
Handles NaN and Infinity values that can't be serialized to JSON
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Any, Dict, List, Union
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
def sanitize_for_json(data: Any) -> Any:
|
||||
"""
|
||||
Recursively sanitize data to make it JSON-serializable.
|
||||
Converts NaN, Infinity, and -Infinity to None.
|
||||
|
||||
Args:
|
||||
data: Any Python object (dict, list, float, etc.)
|
||||
|
||||
Returns:
|
||||
Sanitized data safe for JSON serialization
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return {key: sanitize_for_json(value) for key, value in data.items()}
|
||||
|
||||
elif isinstance(data, (list, tuple)):
|
||||
return [sanitize_for_json(item) for item in data]
|
||||
|
||||
elif isinstance(data, (pd.DataFrame,)):
|
||||
# Convert DataFrame to dict and sanitize
|
||||
return sanitize_for_json(data.to_dict(orient='records'))
|
||||
|
||||
elif isinstance(data, (pd.Series,)):
|
||||
# Convert Series to list and sanitize
|
||||
return sanitize_for_json(data.tolist())
|
||||
|
||||
elif isinstance(data, (np.integer, np.floating)):
|
||||
# Convert numpy types to Python types
|
||||
data = data.item()
|
||||
return sanitize_for_json(data)
|
||||
|
||||
elif isinstance(data, float):
|
||||
# Handle NaN, Infinity, -Infinity
|
||||
if math.isnan(data) or math.isinf(data):
|
||||
return None
|
||||
return data
|
||||
|
||||
elif pd.isna(data):
|
||||
# Handle pandas NA/NaT
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sanitize_response(response_data: Union[Dict, List]) -> Union[Dict, List]:
|
||||
"""
|
||||
Sanitize API response data before JSON serialization.
|
||||
Use this wrapper for all API endpoint returns.
|
||||
|
||||
Args:
|
||||
response_data: Dict or List response from API endpoint
|
||||
|
||||
Returns:
|
||||
Sanitized response safe for JSON encoding
|
||||
"""
|
||||
return sanitize_for_json(response_data)
|
||||
+5
-4
@@ -46,11 +46,12 @@ async def lifespan(app: FastAPI):
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Database initialization warning: {e}")
|
||||
|
||||
# Start data updater service
|
||||
# Start data updater service (temporarily disabled to avoid blocking)
|
||||
# TODO: Fix data_updater to use truly async operations
|
||||
try:
|
||||
data_updater = get_data_updater()
|
||||
await data_updater.start()
|
||||
logger.info("✅ Data updater service started")
|
||||
# data_updater = get_data_updater()
|
||||
# await data_updater.start()
|
||||
logger.info("⚠️ Data updater service temporarily disabled (blocking issue)")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Data updater service start warning: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user