fix(trader): add mutex to prevent race condition in Meta refresh (#796)

* fix(trader): add mutex to prevent race condition in Meta refresh (issue #742)
**問題**:
根據 issue #742 審查標準,發現 BLOCKING 級別的並發安全問題:
- refreshMetaIfNeeded() 中的 `t.meta = meta` 缺少並發保護
- 多個 goroutine 同時調用 OpenLong/OpenShort 會造成競態條件
**修復**:
1. 添加 sync.RWMutex 保護 meta 字段
2. refreshMetaIfNeeded() 使用寫鎖保護 meta 更新
3. getSzDecimals() 使用讀鎖保護 meta 訪問
**符合標準**:
- issue #742: "並發安全問題需使用 sync.Once 等機制"
- 使用 RWMutex 實現讀寫分離,提升並發性能
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* test(trader): add comprehensive race condition tests for meta field mutex protection
- Test concurrent reads (100 goroutines accessing getSzDecimals)
- Test concurrent read/write (50 readers + 10 writers simulating meta refresh)
- Test nil meta edge case (returns default value 4)
- Test valid meta with multiple coins (BTC, ETH, SOL)
- Test massive concurrency (1000 iterations with race detector)
All 5 test cases passed, including -race verification with no data races detected.
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com>
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
0xYYBB | ZYY | Bobo
2025-11-11 09:50:56 +08:00
committed by GitHub
parent 6a66913194
commit 57e31b2ace
2 changed files with 233 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ import (
"log"
"strconv"
"strings"
"sync"
"github.com/ethereum/go-ethereum/crypto"
"github.com/sonirico/go-hyperliquid"
@@ -19,6 +20,7 @@ type HyperliquidTrader struct {
ctx context.Context
walletAddr string
meta *hyperliquid.Meta // 缓存meta信息包含精度等
metaMutex sync.RWMutex // 保护meta字段的并发访问
isCrossMargin bool // 是否为全仓模式
}
@@ -334,6 +336,41 @@ func (t *HyperliquidTrader) SetLeverage(symbol string, leverage int) error {
return nil
}
// refreshMetaIfNeeded 当 Meta 信息失效时刷新Asset ID 为 0 时触发)
func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error {
assetID := t.exchange.Info().NameToAsset(coin)
if assetID != 0 {
return nil // Meta 正常,无需刷新
}
log.Printf("⚠️ %s 的 Asset ID 为 0尝试刷新 Meta 信息...", coin)
// 刷新 Meta 信息
meta, err := t.exchange.Info().Meta(t.ctx)
if err != nil {
return fmt.Errorf("刷新 Meta 信息失败: %w", err)
}
// ✅ 并发安全:使用写锁保护 meta 字段更新
t.metaMutex.Lock()
t.meta = meta
t.metaMutex.Unlock()
log.Printf("✅ Meta 信息已刷新,包含 %d 个资产", len(meta.Universe))
// 验证刷新后的 Asset ID
assetID = t.exchange.Info().NameToAsset(coin)
if assetID == 0 {
return fmt.Errorf("❌ 即使在刷新 Meta 后,资产 %s 的 Asset ID 仍为 0。可能原因\n"+
" 1. 该币种未在 Hyperliquid 上市\n"+
" 2. 币种名称错误(应为 BTC 而非 BTCUSDT\n"+
" 3. API 连接问题", coin)
}
log.Printf("✅ 刷新后 Asset ID 检查通过: %s -> %d", coin, assetID)
return nil
}
// OpenLong 开多仓
func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先取消该币种的所有委托单
@@ -778,6 +815,10 @@ func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (str
// getSzDecimals 获取币种的数量精度
func (t *HyperliquidTrader) getSzDecimals(coin string) int {
// ✅ 并发安全:使用读锁保护 meta 字段访问
t.metaMutex.RLock()
defer t.metaMutex.RUnlock()
if t.meta == nil {
log.Printf("⚠️ meta信息为空使用默认精度4")
return 4 // 默认精度