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:
2025-12-25 20:07:05 +08:00
co-authored by Claude Sonnet 4.5
parent c3a401e8e0
commit e8c19ca20c
4 changed files with 321 additions and 6 deletions
+244
View File
@@ -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
+6 -2
View File
@@ -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:
+66
View File
@@ -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
View File
@@ -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}")