Repository: PBH-BTN/trunker
Pull request #21: skip map use ringbuffer
Author: login: Gaojianli
Created: 2025-03-13T03:47:26Z | Merged: 2025-03-17T11:08:53Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit


- **新功能**
  - 引入了分组管理机制，优化了对等节点的数据存储与访问，提升了并发处理能力和内存效率。
  - 新增多个方法以支持对等节点的加载、存储和计数操作。
  - 增加了统计信息的获取频率控制，优化了性能。
  - 引入临时文件处理机制，增强了文件操作的错误处理。
  - 新增配置选项以控制缓存功能的启用状态。
  - 更新了文档，详细描述了新的缓存和RocketMQ配置选项。
  - 新增`TrackerId`字段，增强了响应信息的完整性。
  - 引入WebSocket管理器，支持WebSocket连接的对等节点管理。

- **重构**
  - 调整了节点清理流程，使无效节点更快移除，从而增强了系统在高负载情况下的稳定性和响应速度。
  - 更新了依赖项以确保兼容性和性能优化。

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/biz/config/init.go b/biz/config/init.go
index 023c84a..ab1cb5c 100644
--- a/biz/config/init.go
+++ b/biz/config/init.go
@@ -26,8 +26,9 @@ type RocketMqConfig struct {
 }
 
 type RedisConfig struct {
-	Host string `yaml:"host" json:"host"`
-	Port int    `yaml:"port" json:"port"`
+	Enable bool   `yaml:"enable" json:"enable"`
+	Host   string `yaml:"host" json:"host"`
+	Port   int    `yaml:"port" json:"port"`
 }
 
 type runningMode string
@@ -42,6 +43,7 @@ type TrackerConfig struct {
 	Database            databaseConfig `yaml:"database" json:"database"`
 	Memory              memoryConfig   `yaml:"memory" json:"memory"`
 	UDPServer           udpConfig      `yaml:"udpServer" json:"udpServer"`
+	WSServer            wsConfig       `yaml:"wsServer" json:"wsServer"`
 	Mode                runningMode    `yaml:"mode" json:"mode"`
 	RPC                 rpcConfig      `yaml:"rpc" json:"rpc"`
 	HostPorts           string         `yaml:"hostPorts" json:"hostPorts"`
@@ -52,6 +54,12 @@ type TrackerConfig struct {
 	UseAnnounceIP       bool           `yaml:"useAnnounceIP" json:"useAnnounceIP"` // allow peer to announce it external ip
 	EnableEventProducer bool           `yaml:"enableEventProducer" json:"enableEventProducer"`
 	EnableMetrics       bool           `yaml:"enableMetrics" json:"enableMetrics"`
+	TrackerId           string         `yaml:"trackerId" json:"trackerId"`
+}
+
+type wsConfig struct {
+	Enable bool `yaml:"enable" json:"enable"`
+	Shard  int  `yaml:"shard" json:"shard"`
 }
 
 type rpcConfig struct {
@@ -70,7 +78,6 @@ type memoryConfig struct {
 	MaxPeersPerTorrent int    `yaml:"maxPeersPerTorrent" json:"maxPeersPerTorrent"`
 	Shard              int    `yaml:"shard" json:"shard"`
 	EnablePersist      bool   `yaml:"enablePersist" json:"enablePersist"`
-	EnableWS           bool   `yaml:"enableWS" json:"enableWS"`
 }
 
 type Config struct {
@@ -96,6 +103,9 @@ func injectDefaultValue(conf *Config) {
 	if conf.Tracker.MetricsHostPorts == "" {
 		conf.Tracker.MetricsHostPorts = "127.0.0.1:9091"
 	}
+	if conf.Tracker.TrackerId == "" {
+		conf.Tracker.TrackerId = "default"
+	}
 }
 func Init() {
 	config := &Config{}
diff --git a/biz/handler/announce.go b/biz/handler/announce.go
index 8c23053..0a304b2 100644
--- a/biz/handler/announce.go
+++ b/biz/handler/announce.go
@@ -23,7 +23,7 @@ import (
 )
 
 func Announce(ctx context.Context, c *app.RequestContext) {
-	if config.AppConfig.Tracker.Mode == config.RunningModeMemory && config.AppConfig.Tracker.Memory.EnableWS {
+	if config.AppConfig.Tracker.Mode == config.RunningModeMemory && config.AppConfig.Tracker.WSServer.Enable {
 		if strings.EqualFold(c.Request.Header.Get(consts.HeaderConnection), "Upgrade") && strings.EqualFold(c.Request.Header.Get("Upgrade"), "websocket") {
 			HandleWebTorrent(ctx, c)
 			return
@@ -77,6 +77,7 @@ func Announce(ctx context.Context, c *app.RequestContext) {
 			}),
 			ExternalIp: conv.UnsafeBytesToString(req.ClientIP),
 			Incomplete: scrape.Incomplete,
+			TrackerId:  config.AppConfig.Tracker.TrackerId,
 			Complete:   scrape.Complete,
 		})
 	} else {
@@ -84,6 +85,7 @@ func Announce(ctx context.Context, c *app.RequestContext) {
 			"interval":    config.AppConfig.Tracker.TTL + int64(fastrand.Intn(201)-100),
 			"external ip": conv.UnsafeBytesToString(req.ClientIP),
 			"incomplete":  scrape.Incomplete,
+			"tracker id":  config.AppConfig.Tracker.TrackerId,
 			"complete":    scrape.Complete,
 		}
 		peers, peers6 := common.PeersToCompact(res)
diff --git a/biz/handler/webtorrent.go b/biz/handler/webtorrent.go
index a8203c6..67c5bc6 100644
--- a/biz/handler/webtorrent.go
+++ b/biz/handler/webtorrent.go
@@ -2,13 +2,14 @@ package handler
 
 import (
 	"context"
+	"encoding/hex"
 	"encoding/json"
 	"errors"
 
 	"github.com/PBH-BTN/trunker/biz/config"
 	"github.com/PBH-BTN/trunker/biz/model"
-	"github.com/PBH-BTN/trunker/biz/services/peer"
 	"github.com/PBH-BTN/trunker/biz/services/peer/common"
+	peer "github.com/PBH-BTN/trunker/biz/services/peer/websocket"
 	"github.com/PBH-BTN/trunker/service/metrics"
 	"github.com/PBH-BTN/trunker/utils/bittorrent"
 	"github.com/PBH-BTN/trunker/utils/conv"
@@ -64,6 +65,10 @@ func HandleWebTorrent(ctx context.Context, c *app.RequestContext) {
 				if answerRaw, err = sonic.Get(message, "answer"); err == nil {
 					if answerRaw.Valid() {
 						err = handleWSAnswer(ctx, message)
+						if err != nil {
+							hlog.CtxErrorf(ctx, "handle answer error: %s", err.Error())
+							err = nil
+						}
 						break
 					}
 				}
@@ -90,6 +95,7 @@ func HandleWebTorrent(ctx context.Context, c *app.RequestContext) {
 	}
 }
 func handleWSAnswer(ctx context.Context, msg []byte) error {
+	hlog.CtxDebugf(ctx, "websocket answer: %s", msg)
 	infoHashRaw, err := sonic.Get(msg, "info_hash")
 	if err != nil {
 		return err
@@ -107,8 +113,9 @@ func handleWSAnswer(ctx context.Context, msg []byte) error {
 	if err != nil {
 		return err
 	}
-	peerId = string(conv.TransUTF8To8859_1(conv.UnsafeStringToBytes(infoHash)))
-	return peer.GetPeerManager().AnswerToPeer(ctx, infoHash, peerId, msg)
+	peerId = string(conv.TransUTF8To8859_1(conv.UnsafeStringToBytes(peerId)))
+	hlog.CtxDebugf(ctx, "[info_hash %s] answer to peer %s", hex.EncodeToString(conv.UnsafeStringToBytes(infoHash)), peerId)
+	return peer.GetWSManager().AnswerToPeer(ctx, infoHash, peerId, msg)
 }
 
 func handleWSAnnounce(ctx context.Context, msg []byte, c *app.RequestContext, conn *model.Conn) error {
@@ -140,11 +147,11 @@ func handleWSAnnounce(ctx context.Context, msg []byte, c *app.RequestContext, co
 	req.Conn = conn
 	req.Type = model.PeerTypeWebtorrent
 	req.Source = model.SourceWS
-	res, err := peer.GetPeerManager().HandleAnnouncePeer(ctx, &req)
+	res, err := peer.GetWSManager().HandleAnnouncePeer(ctx, &req)
 	if err != nil {
 		return err
 	}
-	scrape, err := peer.GetPeerManager().Scrape(ctx, req.InfoHash)
+	scrape, err := peer.GetWSManager().Scrape(ctx, req.InfoHash)
 	if err != nil {
 		return err
 	}
@@ -159,25 +166,26 @@ func handleWSAnnounce(ctx context.Context, msg []byte, c *app.RequestContext, co
 		hlog.CtxErrorf(ctx, "write response error: %s", err.Error())
 		return err
 	}
-	if len(req.Offers) == len(res) {
-		for i, to := range res {
-			if to.ID != req.PeerID && i < len(req.Offers) {
-				if err := sendOffer(ctx, to, req.Offers[i], req.InfoHash, req.PeerID); err != nil {
-					hlog.CtxErrorf(ctx, "failed to send offer: %s", err.Error())
+	if len(req.Offers) > 0 {
+		if len(req.Offers) == len(res) {
+			for i, to := range res {
+				if to.ID != req.PeerID && i < len(req.Offers) {
+					if err := sendOffer(ctx, to, req.Offers[i], req.InfoHash, req.PeerID); err != nil {
+						hlog.CtxErrorf(ctx, "failed to send offer: %s", err.Error())
+					}
 				}
 			}
-		}
-	} else {
-		picker := choose.Slice(req.Offers)
-		for _, p := range res {
-			if p.ID != req.PeerID {
-				if err := sendOffer(ctx, p, picker.One(), req.InfoHash, req.PeerID); err != nil {
-					hlog.CtxErrorf(ctx, "failed to send offer: %s", err.Error())
+		} else {
+			picker := choose.Slice(req.Offers)
+			for _, p := range res {
+				if p.ID != req.PeerID {
+					if err := sendOffer(ctx, p, picker.One(), req.InfoHash, req.PeerID); err != nil {
+						hlog.CtxErrorf(ctx, "failed to send offer: %s", err.Error())
+					}
 				}
 			}
 		}
 	}
-
 	return nil
 
 }
@@ -191,20 +199,20 @@ func handleWSScrape(ctx context.Context, req []byte, conn *model.Conn) error {
 	if tryArray, err := infoHashRaw.Array(); err == nil {
 		for _, v := range tryArray {
 			if s, ok := v.(string); ok {
-				infoHashes = append(infoHashes, string(conv.TransUTF8To8859_1(conv.UnsafeStringToBytes(s))))
+				infoHashes = append(infoHashes, s)
 			}
 		}
 	} else if tryString, err := infoHashRaw.String(); err == nil {
-		infoHashes = append(infoHashes, string(conv.TransUTF8To8859_1(conv.UnsafeStringToBytes(tryString))))
+		infoHashes = append(infoHashes, tryString)
 	}
 	if len(infoHashes) == 0 {
 		return errors.New("info_hash can't be empty")
 	}
 	ret := make(map[string]*model.ScrapeFile)
-	manager := peer.GetPeerManager()
+	manager := peer.GetWSManager()
 	for _, infoHash := range infoHashes {
 		var err error
-		ret[infoHash], err = manager.Scrape(ctx, infoHash)
+		ret[infoHash], err = manager.Scrape(ctx, string(conv.TransUTF8To8859_1(conv.UnsafeStringToBytes(infoHash))))
 		if err != nil {
 			return err
 		}
diff --git a/biz/model/http.go b/biz/model/http.go
index 56a760e..6a43e44 100644
--- a/biz/model/http.go
+++ b/biz/model/http.go
@@ -55,6 +55,7 @@ type AnnounceBasicResponse struct {
 	Interval   int64    `json:"interval" bencode:"interval"`
 	Peers      []*Peer  `json:"peers" bencode:"peers"`
 	ExternalIp string   `json:"externalIp" bencode:"external ip"`
+	TrackerId  string   `json:"trackerId" bencode:"tracker id"`
 	Complete   int      `json:"complete" bencode:"complete"`
 	Incomplete int      `json:"incomplete" bencode:"incomplete"`
 	Offers     []*Offer `json:"offers,omitempty" bencode:"offers,omitempty"`
@@ -71,6 +72,7 @@ type OfferDetail struct {
 type ErrorResponse struct {
 	FailureReason string `json:"failureReason" bencode:"failure reason"`
 	Retry         string `json:"retry" bencode:"retry in"`
+	TrackerId     string `json:"trackerId" bencode:"tracker id"`
 }
 
 type ScrapeRequest struct {
diff --git a/biz/services/peer/local/clean.go b/biz/services/peer/local/clean.go
index e400fa2..0f2f2fe 100644
--- a/biz/services/peer/local/clean.go
+++ b/biz/services/peer/local/clean.go
@@ -31,8 +31,9 @@ func (m *Manager) cleanUp(root *InfoHashRoot) int64 {
 		return 0
 	}
 	toClean := make([]string, 0)
-	root.peerMap.Range(func(key string, value *common.Peer) bool {
-		if time.Now().Add(time.Duration(-1*config.AppConfig.Tracker.TTL) * time.Second).After(value.LastSeen) {
+	expireTime := time.Now().Add(time.Duration(-1*config.AppConfig.Tracker.TTL) * time.Second)
+	root.Range(func(key string, value *common.Peer) bool {
+		if expireTime.After(value.LastSeen) {
 			toClean = append(toClean, key)
 			if value.Type == model.PeerTypeWebtorrent && value.Conn != nil {
 				_ = value.Conn.Close()
@@ -42,10 +43,10 @@ func (m *Manager) cleanUp(root *InfoHashRoot) int64 {
 		return true
 	})
 	for _, key := range toClean {
-		root.peerMap.Delete(key)
+		root.LoadAndDelete(key)
 	}
 	root.lastClean = time.Now()
-	if root.peerMap.Len() == 0 {
+	if root.Len() == 0 {
 		m.infoHashMap.Delete(root.infoHash)
 	}
 	return int64(len(toClean))
diff --git a/biz/services/peer/local/local.go b/biz/services/peer/local/local.go
index 7f08eac..7ad1941 100644
--- a/biz/services/peer/local/local.go
+++ b/biz/services/peer/local/local.go
@@ -6,7 +6,6 @@ import (
 	"errors"
 	"net"
 	"runtime"
-	"strings"
 	"sync/atomic"
 	"time"
 
@@ -14,29 +13,74 @@ import (
 	"github.com/PBH-BTN/trunker/biz/model"
 	"github.com/PBH-BTN/trunker/biz/services/peer/common"
 	"github.com/PBH-BTN/trunker/biz/services/producer"
+	"github.com/PBH-BTN/trunker/service/cache"
 	"github.com/PBH-BTN/trunker/utils"
 	"github.com/PBH-BTN/trunker/utils/collections/mapx"
 	"github.com/PBH-BTN/trunker/utils/conv"
 	"github.com/bytedance/gopkg/util/gopool"
-	json "github.com/bytedance/sonic"
 	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/xxjwxc/gowp/workpool"
 )
 
 type InfoHashRoot struct {
-	peerMap   mapx.SyncStringMap[*common.Peer]
-	lastClean time.Time
-	infoHash  string
+	peerMap       [3]mapx.SyncStringMap[*common.Peer] // always keep 3 map, one for write, one for readonly and one keeps empty
+	lastClean     time.Time
+	infoHash      string
+	currentActive uint32
 }
 
 func NewInfoHashRoot(infoHash string) *InfoHashRoot {
 	return &InfoHashRoot{
-		peerMap:   mapx.NewSkipMap[*common.Peer](),
+		currentActive: 0,
+		peerMap: [3]mapx.SyncStringMap[*common.Peer]{
+			mapx.NewSkipMap[*common.Peer](),
+			mapx.NewSkipMap[*common.Peer](),
+			mapx.NewSkipMap[*common.Peer](),
+		},
 		lastClean: time.Now(),
 		infoHash:  infoHash,
 	}
 }
 
+func (i *InfoHashRoot) Load(key string) (*common.Peer, bool) {
+	for _, peerMap := range i.peerMap {
+		if v, ok := peerMap.Load(key); ok {
+			return v, ok
+		}
+	}
+	return nil, false
+}
+
+func (i *InfoHashRoot) LoadAndDelete(key string) (*common.Peer, bool) {
+	var foundPeer *common.Peer
+	var found bool
+	for _, peerMap := range i.peerMap {
+		if v, ok := peerMap.LoadAndDelete(key); ok {
+			found = true
+			if foundPeer == nil || v.LastSeen.After(foundPeer.LastSeen) {
+				foundPeer = v
+			}
+		}
+	}
+	return foundPeer, found
+}
+
+func (i *InfoHashRoot) LoadOrStore(key string, peer *common.Peer) (*common.Peer, bool) {
+	return i.peerMap[i.currentActive].LoadOrStore(key, peer)
+}
+
+func (i *InfoHashRoot) Store(key string, peer *common.Peer) {
+	i.peerMap[i.currentActive].Store(key, peer)
+}
+
+func (i *InfoHashRoot) Len() int {
+	count := 0
+	for _, s := range i.peerMap {
+		count += s.Len()
+	}
+	return count
+}
+
 type Manager struct {
 	infoHashMap mapx.SyncStringMap[*InfoHashRoot]
 }
@@ -81,21 +125,25 @@ func (m *Manager) HandleAnnouncePeer(ctx context.Context, req *model.AnnounceReq
 	if peer.Type == model.PeerTypeWebtorrent && req.Conn != nil {
 		peer.Conn.CloseCallback = func() {
 			hlog.CtxDebugf(ctx, "delete peer %s from %s due to connect close", peer.ID, hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)))
-			if v, ok := root.peerMap.LoadAndDelete(req.PeerID); ok {
+			if v, ok := root.LoadAndDelete(req.PeerID); ok {
 				v.Conn = nil
 			}
 		}
 	}
 	if !ok { // first seen torrent
 		if common.IsPeerConnectable(peer) {
-			root.peerMap.LoadOrStore(peer.GetKey(), peer)
+			root.LoadOrStore(peer.GetKey(), peer)
 		}
 		go producer.SendPeerEvent(ctx, req.InfoHash, peer)
 		return nil, nil
 	}
+	if peer.Event == common.PeerEvent_Stopped { // stopped peer must remove and return nothing
+		root.LoadAndDelete(peer.GetKey())
+		return nil, nil
+	}
 	// add to peer list
 	gopool.CtxGo(ctx, func() {
-		if knownPeer, ok := root.peerMap.Load(peer.GetKey()); ok {
+		if knownPeer, ok := root.LoadAndDelete(peer.GetKey()); ok {
 			// update current record
 			if (knownPeer.Left != 0 && peer.Left == 0) || knownPeer.Event != peer.Event {
 				go producer.SendPeerEvent(ctx, req.InfoHash, peer)
@@ -106,27 +154,18 @@ func (m *Manager) HandleAnnouncePeer(ctx context.Context, req *model.AnnounceReq
 			knownPeer.Event = peer.Event
 			knownPeer.Left = peer.Left
 			knownPeer.Event = peer.Event
+			root.Store(knownPeer.GetKey(), knownPeer)
 		} else {
 			// new peer!
 			if common.IsPeerConnectable(peer) { // skip private ip
-				root.peerMap.LoadOrStore(peer.GetKey(), peer)
+				root.LoadOrStore(peer.GetKey(), peer)
 				go producer.SendPeerEvent(ctx, req.InfoHash, peer)
 			}
 		}
 	})
 	// get return
-	resp := make([]*common.Peer, 0, utils.Positive(min(root.peerMap.Len(), req.NumWant)))
-	timeoutPeer := make([]*common.Peer, 0)
-	var oldestTime *time.Time
-	var oldestPeer *common.Peer
-	shouldEject := root.peerMap.Len() > config.AppConfig.Tracker.Memory.MaxPeersPerTorrent
-	expireTime := time.Now().Add(time.Duration(-1*config.AppConfig.Tracker.TTL) * time.Second) // last seen time after this should be ejected
-	root.peerMap.Range(func(_ string, value *common.Peer) bool {
-		if expireTime.After(value.LastSeen) {
-			// timeout!
-			timeoutPeer = append(timeoutPeer, value)
-			return true
-		}
+	resp := make([]*common.Peer, 0, utils.Positive(min(root.Len(), req.NumWant)))
+	root.Range(func(_ string, value *common.Peer) bool {
 		if value.Type != peer.Type { // same type peer only
 			return true
 		}
@@ -138,17 +177,7 @@ func (m *Manager) HandleAnnouncePeer(ctx context.Context, req *model.AnnounceReq
 		if value.Event == common.PeerEvent_Stopped { // stopped peer should not return
 			return true
 		}
-		if shouldEject {
-			if oldestTime == nil {
-				oldestTime = &value.LastSeen
-				oldestPeer = value
-			} else {
-				if value.LastSeen.Before(*oldestTime) {
-					oldestTime = &value.LastSeen
-					oldestPeer = value
-				}
-			}
-		}
+
 		if len(resp) >= req.NumWant {
 			return false
 		}
@@ -158,23 +187,26 @@ func (m *Manager) HandleAnnouncePeer(ctx context.Context, req *model.AnnounceReq
 		resp = append(resp, value)
 		return true
 	})
-	if len(timeoutPeer) > 0 {
-		gopool.CtxGo(ctx, func() {
-			for _, toClean := range timeoutPeer {
-				root.peerMap.Delete(toClean.GetKey())
-			}
-		})
-	}
-	if shouldEject && oldestPeer != nil {
-		gopool.CtxGo(ctx, func() {
-			hlog.CtxDebugf(ctx, "info hash %s ejected %s:%d(%s) %s, last seen:%s", hex.EncodeToString(conv.UnsafeStringToBytes(root.infoHash)), oldestPeer.GetIP().String(), oldestPeer.Port, oldestPeer.ID, oldestPeer.UserAgent, oldestTime.Format(time.DateTime))
-			root.peerMap.Delete(oldestPeer.GetKey())
-		})
+	if root.peerMap[root.currentActive].Len() > config.AppConfig.Tracker.Memory.MaxPeersPerTorrent/2 { // reach max, start to eject
+		hlog.CtxDebugf(ctx, "[info_hash %s] active set full, currentActive %d, size: 0: %d 1:%d 2:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), root.currentActive, root.peerMap[0].Len(), root.peerMap[1].Len(), root.peerMap[2].Len())
+		current := root.currentActive
+		if atomic.CompareAndSwapUint32(&root.currentActive, current, (current+1)%3) { // write head switch to next
+			hlog.CtxDebugf(ctx, "[info_hash %s] active set swapped! current:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), root.currentActive)
+			// empty the oldest map
+			hlog.CtxDebugf(ctx, "[info_hash %s] clean oldest set %d, len:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), (current+2)%3, root.peerMap[(current+2)%3].Len())
+			root.peerMap[(current+2)%3] = mapx.NewSkipMap[*common.Peer]()
+		}
 	}
+
 	return resp, nil
 }
 
 func (m *Manager) Scrape(ctx context.Context, infoHash string) (*model.ScrapeFile, error) {
+	if config.AppConfig.Cache.Enable {
+		if v, ok := cache.Get[model.ScrapeFile](ctx, "scrape_"+infoHash); ok {
+			return v, nil
+		}
+	}
 	root, ok := m.infoHashMap.Load(infoHash)
 	if !ok {
 		return &model.ScrapeFile{
@@ -186,38 +218,45 @@ func (m *Manager) Scrape(ctx context.Context, infoHash string) (*model.ScrapeFil
 	}
 	var complete, incomplete, downloaded, seeder atomic.Int64
 	p := workpool.New(runtime.NumCPU() * 2)
-	root.peerMap.Range(func(_ string, value *common.Peer) bool {
-		p.Do(func() error {
-			if value.Left == 0 {
-				downloaded.Add(1)
-				complete.Add(1)
-				if value.Event != common.PeerEvent_Stopped {
-					seeder.Add(1)
+	for _, s := range root.peerMap {
+		s.Range(func(_ string, value *common.Peer) bool {
+			p.Do(func() error {
+				if value.Left == 0 {
+					downloaded.Add(1)
+					complete.Add(1)
+					if value.Event != common.PeerEvent_Stopped {
+						seeder.Add(1)
+					}
+					return nil
+				}
+				if value.Event == common.PeerEvent_Completed {
+					complete.Add(1)
+				} else {
+					incomplete.Add(1)
 				}
 				return nil
-			}
-			if value.Event == common.PeerEvent_Completed {
-				complete.Add(1)
-			} else {
-				incomplete.Add(1)
-			}
-			return nil
+			})
+			return true
 		})
-		return true
-	})
+	}
+
 	_ = p.Wait()
-	return &model.ScrapeFile{
+	ret := &model.ScrapeFile{
 		Seeder:     int(seeder.Load()),
 		Complete:   int(complete.Load()),
 		Incomplete: int(incomplete.Load()),
 		Downloaded: int(downloaded.Load()), // 这个目前不实现
-	}, nil
+	}
+	if config.AppConfig.Cache.Enable {
+		_ = cache.Set(ctx, "scrape_"+infoHash, ret, time.Minute*5)
+	}
+	return ret, nil
 }
 
 func (m *Manager) GetStatistic(_ context.Context) *common.StatisticInfo {
 	peerCount := 0
 	m.infoHashMap.Range(func(_ string, value *InfoHashRoot) bool {
-		peerCount += value.peerMap.Len()
+		peerCount += value.Len()
 		return true
 	})
 	return &common.StatisticInfo{
@@ -235,11 +274,13 @@ func (m *Manager) DirectStore(infoHash string, peer *common.Peer) {
 	root, _ := m.infoHashMap.LoadOrStoreLazy(infoHash, func() *InfoHashRoot {
 		return NewInfoHashRoot(infoHash)
 	})
-	root.peerMap.Store(peer.GetKey(), peer)
+	root.Store(peer.GetKey(), peer)
 }
 
 func (i *InfoHashRoot) Range(f func(key string, value *common.Peer) bool) {
-	i.peerMap.Range(f)
+	for _, s := range i.peerMap {
+		s.Range(f)
+	}
 }
 
 func (m *Manager) StoreToPersist() {
@@ -274,36 +315,10 @@ func (m *Manager) GetPeers(_ context.Context, infoHash string) ([]*common.Peer,
 	if !ok {
 		return []*common.Peer{}, nil
 	}
-	return utils.SkipMapToSlice(peerMap.peerMap), nil
+	return utils.SkipMapToSlice(peerMap), nil
 }
 
 func (m *Manager) DeleteInfoHash(_ context.Context, infoHash string) error {
 	m.infoHashMap.Delete(infoHash)
 	return nil
 }
-
-func (m *Manager) AnswerToPeer(ctx context.Context, infoHash string, peerID string, answerBody []byte) error {
-	root, ok := m.infoHashMap.Load(infoHash)
-	if !ok {
-		return errors.New("info hash not found")
-	}
-	peer, ok := root.peerMap.Load(peerID)
-	if !ok {
-		return errors.New("peer not found")
-	}
-	if peer.Conn == nil {
-		return errors.New("peer not connected")
-	}
-	resp := map[string]any{}
-	_ = json.Unmarshal(answerBody, &resp)
-	delete(resp, "to_peer_id")
-	err := peer.Conn.WriteJSON(resp)
-	if err != nil {
-		if strings.Contains(err.Error(), "close") {
-			peer.Conn = nil
-			return errors.New("remote peer offline")
-		}
-		return err
-	}
-	return nil
-}
diff --git a/biz/services/peer/manager.go b/biz/services/peer/manager.go
index ce61a8a..1599741 100644
--- a/biz/services/peer/manager.go
+++ b/biz/services/peer/manager.go
@@ -22,8 +22,6 @@ type PeerManager interface {
 	LoadFromPersist()
 	// StoreToPersist 保存数据到持久化存储
 	StoreToPersist()
-	// AnswerToPeer send message to peer, Only for webtorrent
-	AnswerToPeer(ctx context.Context, infoHash string, peerID string, answerBody []byte) error
 
 	/*	admin interface	*/
 	GetStatistic(ctx context.Context) (*common.StatisticInfo, error)
diff --git a/biz/services/peer/mux_local/mux_manager.go b/biz/services/peer/mux_local/mux_manager.go
index 2b99e0c..aaafa43 100644
--- a/biz/services/peer/mux_local/mux_manager.go
+++ b/biz/services/peer/mux_local/mux_manager.go
@@ -9,7 +9,6 @@ import (
 	"sync"
 	"sync/atomic"
 
-	"github.com/PBH-BTN/trunker/biz/config"
 	"github.com/PBH-BTN/trunker/biz/model"
 	"github.com/PBH-BTN/trunker/biz/services/peer/common"
 	"github.com/PBH-BTN/trunker/biz/services/peer/local"
@@ -25,10 +24,7 @@ type MuxLocalManager struct {
 }
 
 func NewMuxLocalManager(num int) *MuxLocalManager {
-	hlog.Info("running as memory mode")
-	if config.AppConfig.Tracker.Memory.EnableWS {
-		hlog.Info("websocket support enabled")
-	}
+	hlog.Info("running as memory mode, shard: ", num)
 	list := make([]*local.Manager, 0, num)
 	for i := 0; i < num; i++ {
 		list = append(list, local.NewLocalManger())
@@ -134,8 +130,3 @@ func (m *MuxLocalManager) DeleteInfoHash(ctx context.Context, infoHash string) e
 	worker := m.pickWorker(conv.UnsafeStringToBytes(infoHash))
 	return worker.DeleteInfoHash(ctx, infoHash)
 }
-
-func (m *MuxLocalManager) AnswerToPeer(ctx context.Context, infoHash string, peerID string, answerBody []byte) error {
-	worker := m.pickWorker(conv.UnsafeStringToBytes(infoHash))
-	return worker.AnswerToPeer(ctx, infoHash, peerID, answerBody)
-}
diff --git a/biz/services/peer/mux_local/persist.go b/biz/services/peer/mux_local/persist.go
index 12818a4..b416eea 100644
--- a/biz/services/peer/mux_local/persist.go
+++ b/biz/services/peer/mux_local/persist.go
@@ -2,12 +2,15 @@ package mux_local
 
 import (
 	"bufio"
+	"crypto/rand"
 	"encoding/binary"
+	"fmt"
 	"io"
 	"os"
 	"strings"
 	"time"
 
+	"github.com/DataDog/zstd"
 	"github.com/PBH-BTN/trunker/biz/config"
 	"github.com/PBH-BTN/trunker/biz/services/peer/common"
 	"github.com/PBH-BTN/trunker/biz/services/peer/local"
@@ -29,21 +32,27 @@ func (m *MuxLocalManager) LoadFromPersist() {
 		logger.Errorf("open file error:%s", err.Error())
 		return
 	}
-	defer file.Close()
-	reader := bufio.NewReader(file)
+	reader := zstd.NewReader(bufio.NewReader(file))
+	defer func() {
+		_ = reader.Close()
+		_ = file.Close()
+	}()
 	count := 0
 	expired := 0
+	now := time.Now()
+	data := make([]byte, 0, 400)
+	raminBuf := make([]byte, 0, 400)
+	var size uint32
 	for {
-		var size uint32
 		// Decode data length
 		if err := binary.Read(reader, binary.LittleEndian, &size); err != nil {
-			if err == io.EOF { // end of file
+			if err == io.EOF || strings.Contains(err.Error(), "EOF") { // end of file
 				break
 			}
 			logger.Errorf("Failed to decode data length:%s", err.Error())
 			return
 		}
-		data := make([]byte, size)
+		data = data[:size]
 		if readCount, err := reader.Read(data); err != nil {
 			logger.Errorf("Failed to decode data length:%s", err.Error())
 			return
@@ -51,25 +60,25 @@ func (m *MuxLocalManager) LoadFromPersist() {
 			// read more
 			remain := size - uint32(readCount)
 			for remain > 0 {
-				tmp := make([]byte, remain)
-				n, err := reader.Read(tmp)
+				raminBuf = raminBuf[:remain]
+				n, err := reader.Read(raminBuf)
 				if err != nil {
 					logger.Errorf("Failed to decode data length:%s", err.Error())
 					return
 				}
 				remain -= uint32(n)
-				data = append(data[0:readCount], tmp...)
+				data = append(data[0:readCount], raminBuf...)
 			}
 		}
 
 		// Unmarshal to protobuf SomeStruct
 		pbStruct := &PeerInfo{}
-		if err := proto.Unmarshal(data, pbStruct); err != nil {
+		if err := proto.Unmarshal(data[:size], pbStruct); err != nil {
 			logger.Errorf("Failed to decode data length:%s", err.Error())
 			break
 		}
 		lastSeen := time.Unix(pbStruct.LastSeen, 0)
-		if lastSeen.Add(time.Duration(config.AppConfig.Tracker.TTL) * time.Second).Before(time.Now()) {
+		if lastSeen.Add(time.Duration(config.AppConfig.Tracker.TTL) * time.Second).Before(now) {
 			// expired, skip
 			expired++
 			continue
@@ -112,22 +121,13 @@ func (m *MuxLocalManager) StoreToPersist() {
 		logger.Infof("persist not enabled, skip...")
 		return
 	}
-	file, err := os.OpenFile(config.AppConfig.Tracker.Memory.PersistFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+	tempFile := fmt.Sprintf("%s.%s", config.AppConfig.Tracker.Memory.PersistFile, rand.Text()[:8])
+	file, err := os.OpenFile(tempFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
 	if err != nil {
 		logger.Errorf("open file error")
 		return
 	}
-	defer file.Close()
-	lock := flock.New(config.AppConfig.Tracker.Memory.PersistFile)
-	if err := lock.Lock(); err != nil {
-		logger.Errorf("failed to obtain write lock: %s", err.Error())
-		return
-	}
-	defer func() {
-		_ = lock.Unlock()
-	}()
-
-	writer := bufio.NewWriter(file)
+	writer := zstd.NewWriterLevel(bufio.NewWriter(file), 10)
 	logger.Infof("start to store peers to persist")
 	count := 0
 	for _, manager := range m.localList {
@@ -183,5 +183,25 @@ func (m *MuxLocalManager) StoreToPersist() {
 		})
 	}
 	_ = writer.Flush()
+	_ = writer.Close()
+	err = file.Close()
+	if err != nil {
+		logger.Errorf("close file error:%s", err.Error())
+		return
+	}
+	lock := flock.New(config.AppConfig.Tracker.Memory.PersistFile)
+	if err := lock.Lock(); err != nil {
+		logger.Errorf("failed to obtain write lock: %s", err.Error())
+		_ = file.Close()
+		return
+	}
+	defer func() {
+		_ = lock.Unlock()
+	}()
+	err = os.Rename(tempFile, config.AppConfig.Tracker.Memory.PersistFile)
+	if err != nil {
+		logger.Errorf("failed to rename file from %s to %s: %s", tempFile, config.AppConfig.Tracker.Memory.PersistFile, err.Error())
+		return
+	}
 	logger.Infof("store to persist done. %d peers stored", count)
 }
diff --git a/biz/services/peer/websocket/local.go b/biz/services/peer/websocket/local.go
new file mode 100644
index 0000000..0c29c13
--- /dev/null
+++ b/biz/services/peer/websocket/local.go
@@ -0,0 +1,213 @@
+package websocket
+
+import (
+	"context"
+	"encoding/hex"
+	"errors"
+	"net"
+	"runtime"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"github.com/PBH-BTN/trunker/biz/config"
+	"github.com/PBH-BTN/trunker/biz/model"
+	"github.com/PBH-BTN/trunker/biz/services/peer/common"
+	"github.com/PBH-BTN/trunker/biz/services/producer"
+	"github.com/PBH-BTN/trunker/service/cache"
+	"github.com/PBH-BTN/trunker/utils"
+	"github.com/PBH-BTN/trunker/utils/collections/mapx"
+	"github.com/PBH-BTN/trunker/utils/conv"
+	"github.com/bytedance/gopkg/util/gopool"
+	json "github.com/bytedance/sonic"
+	"github.com/cloudwego/hertz/pkg/common/hlog"
+	"github.com/xxjwxc/gowp/workpool"
+)
+
+type manager struct {
+	infoHashMap mapx.SyncStringMap[*infoHashRoot]
+}
+
+func newManager() *manager {
+	return &manager{
+		infoHashMap: mapx.NewSkipMap[*infoHashRoot](),
+	}
+}
+
+type infoHashRoot struct {
+	infoHash string
+	peerMap  mapx.SyncStringMap[*common.Peer]
+}
+
+func (m *manager) HandleAnnouncePeer(ctx context.Context, req *model.AnnounceRequest) ([]*common.Peer, error) {
+	peer := &common.Peer{
+		ID:         req.PeerID,
+		IP:         net.ParseIP(req.IP),
+		IPv4:       net.ParseIP(req.IPv4),
+		IPv6:       net.ParseIP(req.IPv6),
+		ClientIP:   req.ClientIP,
+		Uploaded:   req.Uploaded,
+		Left:       req.Left,
+		Port:       req.Port,
+		Type:       req.Type,
+		Downloaded: req.Downloaded,
+		Offers:     req.Offers,
+		LastSeen:   time.Now(),
+		Event:      common.ParsePeerEvent(req.Event),
+		UserAgent:  req.UserAgent,
+		Conn:       req.Conn,
+		Source:     req.Source,
+	}
+	if peer.IPv4 != nil && peer.IPv4.To4() == nil {
+		hlog.CtxWarnf(ctx, "invalid ipv4 address,actual: %s", peer.IPv4.String())
+		return nil, errors.New("invalid address")
+	}
+	if peer.IPv6 != nil && peer.IPv6.To4() != nil {
+		hlog.CtxWarnf(ctx, "invalid ipv6 address,actual: %s", peer.IPv6.String())
+		return nil, errors.New("invalid address")
+	}
+	hlog.CtxDebugf(ctx, "[info_hash %s]handle peer announce %s from %s", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), conv.Trans8859_1ToUTF8([]byte(peer.ID)), peer.GetIP().String())
+
+	root, ok := m.infoHashMap.LoadOrStoreLazy(req.InfoHash, func() *infoHashRoot {
+		return &infoHashRoot{
+			infoHash: req.InfoHash,
+			peerMap:  mapx.NewSkipMap[*common.Peer](),
+		}
+	})
+	peer.Conn.CloseCallback = func() {
+		hlog.CtxDebugf(ctx, "delete peer %s from %s due to connect close, ip: %s", conv.Trans8859_1ToUTF8([]byte(peer.ID)), hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), peer.GetIP().String())
+		if v, ok := root.peerMap.LoadAndDelete(req.PeerID); ok {
+			v.Conn = nil
+		}
+	}
+	if !ok { // first seen torrent
+		root.peerMap.LoadOrStore(peer.GetKey(), peer)
+		go producer.SendPeerEvent(ctx, req.InfoHash, peer)
+		return nil, nil
+	}
+	if peer.Event == common.PeerEvent_Stopped { // stopped peer must remove and return nothing
+		_ = peer.Conn.Close()
+		root.peerMap.Delete(peer.GetKey())
+		return nil, nil
+	}
+	// add to peer list
+	gopool.CtxGo(ctx, func() {
+		if knownPeer, ok := root.peerMap.Load(peer.GetKey()); ok {
+			// update current record
+			if (knownPeer.Left != 0 && peer.Left == 0) || knownPeer.Event != peer.Event {
+				go producer.SendPeerEvent(ctx, req.InfoHash, peer)
+			}
+			knownPeer.Uploaded = peer.Uploaded
+			knownPeer.Downloaded = peer.Downloaded
+			knownPeer.LastSeen = peer.LastSeen
+			knownPeer.Event = peer.Event
+			knownPeer.Left = peer.Left
+			knownPeer.Event = peer.Event
+		} else {
+			// new peer!
+			root.peerMap.LoadOrStore(peer.GetKey(), peer)
+		}
+	})
+	// get return
+	resp := make([]*common.Peer, 0, utils.Positive(min(root.peerMap.Len(), req.NumWant)))
+	root.peerMap.Range(func(_ string, value *common.Peer) bool {
+		if value.Type != peer.Type { // same type peer only
+			return true
+		}
+		if value.Type == model.PeerTypeWebtorrent {
+			if value.Conn == nil {
+				return true
+			}
+		}
+		if value.Event == common.PeerEvent_Stopped { // stopped peer should not return
+			return true
+		}
+
+		if len(resp) >= req.NumWant {
+			return false
+		}
+		if value.ID == peer.ID {
+			return true
+		}
+		resp = append(resp, value)
+		return true
+	})
+
+	return resp, nil
+}
+
+func (m *manager) Scrape(ctx context.Context, infoHash string) (*model.ScrapeFile, error) {
+	if config.AppConfig.Cache.Enable {
+		if v, ok := cache.Get[model.ScrapeFile](ctx, "scrape_ws_"+infoHash); ok {
+			return v, nil
+		}
+	}
+	root, ok := m.infoHashMap.Load(infoHash)
+	if !ok {
+		return &model.ScrapeFile{
+			Complete:   0,
+			Incomplete: 0,
+			Downloaded: 0,
+			Seeder:     0,
+		}, nil
+	}
+	var complete, incomplete, downloaded, seeder atomic.Int64
+	p := workpool.New(runtime.NumCPU() * 2)
+	root.peerMap.Range(func(_ string, value *common.Peer) bool {
+		p.Do(func() error {
+			if value.Left == 0 {
+				downloaded.Add(1)
+				complete.Add(1)
+				if value.Event != common.PeerEvent_Stopped {
+					seeder.Add(1)
+				}
+				return nil
+			}
+			if value.Event == common.PeerEvent_Completed {
+				complete.Add(1)
+			} else {
+				incomplete.Add(1)
+			}
+			return nil
+		})
+		return true
+	})
+
+	_ = p.Wait()
+	ret := &model.ScrapeFile{
+		Seeder:     int(seeder.Load()),
+		Complete:   int(complete.Load()),
+		Incomplete: int(incomplete.Load()),
+		Downloaded: int(downloaded.Load()), // 这个目前不实现
+	}
+	if config.AppConfig.Cache.Enable {
+		_ = cache.Set(ctx, "scrape_ws_"+infoHash, ret, time.Minute*5)
+	}
+	return ret, nil
+}
+
+func (m *manager) AnswerToPeer(ctx context.Context, infoHash string, peerID string, answerBody []byte) error {
+	root, ok := m.infoHashMap.Load(infoHash)
+	if !ok {
+		return errors.New("info hash not found")
+	}
+	peer, ok := root.peerMap.Load(peerID)
+	if !ok {
+		return errors.New("peer not found")
+	}
+	if peer.Conn == nil {
+		return errors.New("peer not connected")
+	}
+	resp := map[string]any{}
+	_ = json.Unmarshal(answerBody, &resp)
+	delete(resp, "to_peer_id")
+	err := peer.Conn.WriteJSON(resp)
+	if err != nil {
+		if strings.Contains(err.Error(), "close") {
+			peer.Conn = nil
+			return errors.New("remote peer offline")
+		}
+		return err
+	}
+	return nil
+}
diff --git a/biz/services/peer/websocket/manager.go b/biz/services/peer/websocket/manager.go
new file mode 100644
index 0000000..25c3f5d
--- /dev/null
+++ b/biz/services/peer/websocket/manager.go
@@ -0,0 +1,74 @@
+package websocket
+
+import (
+	"context"
+	"errors"
+	"math/big"
+
+	"github.com/PBH-BTN/trunker/biz/config"
+	"github.com/PBH-BTN/trunker/biz/model"
+	"github.com/PBH-BTN/trunker/biz/services/peer/common"
+	"github.com/PBH-BTN/trunker/biz/services/peer/mux_local/ban"
+	"github.com/PBH-BTN/trunker/utils/conv"
+	"github.com/cloudwego/hertz/pkg/common/hlog"
+)
+
+type WebsocketMuxManager struct {
+	localList []*manager
+	ban       *ban.Manager
+}
+
+func NewWSMuxLocalManager(num int) *WebsocketMuxManager {
+	hlog.Info("websocket server enable, shard:", num)
+	list := make([]*manager, 0, num)
+	for i := 0; i < num; i++ {
+		list = append(list, newManager())
+	}
+	return &WebsocketMuxManager{
+		localList: list,
+		ban:       ban.NewBanManager(),
+	}
+}
+
+func (m *WebsocketMuxManager) pickWorker(hashBytes []byte) *manager {
+	hashInt := new(big.Int).SetBytes(hashBytes)
+
+	// 取模运算以获得服务索引
+	return m.localList[hashInt.Uint64()%uint64(len(m.localList))]
+
+}
+
+func (m *WebsocketMuxManager) HandleAnnouncePeer(ctx context.Context, req *model.AnnounceRequest) ([]*common.Peer, error) {
+	// process block list
+	if m.ban.Test(ban.BanTypeInfoHash, req.InfoHash) {
+		hlog.CtxInfof(ctx, "info hash %s is banned", req.InfoHash)
+		return nil, errors.New("banned info_hash")
+	}
+	if m.ban.Test(ban.BanTypePeerId, req.PeerID) {
+		hlog.CtxInfof(ctx, "peer id %s is banned", req.PeerID)
+		return nil, errors.New("banned peer_id")
+	}
+
+	worker := m.pickWorker(conv.UnsafeStringToBytes(req.InfoHash))
+	return worker.HandleAnnouncePeer(ctx, req)
+}
+
+func (m *WebsocketMuxManager) Scrape(ctx context.Context, infoHash string) (*model.ScrapeFile, error) {
+	worker := m.pickWorker(conv.UnsafeStringToBytes(infoHash))
+	return worker.Scrape(ctx, infoHash)
+}
+
+var wsManager *WebsocketMuxManager
+
+func InitWSMuxLocalManager() {
+	wsManager = NewWSMuxLocalManager(config.AppConfig.Tracker.WSServer.Shard)
+}
+
+func (m *WebsocketMuxManager) AnswerToPeer(ctx context.Context, infoHash string, peerID string, answerBody []byte) error {
+	worker := m.pickWorker(conv.UnsafeStringToBytes(infoHash))
+	return worker.AnswerToPeer(ctx, infoHash, peerID, answerBody)
+}
+
+func GetWSManager() *WebsocketMuxManager {
+	return wsManager
+}
diff --git a/biz/services/rpc/adaptor.go b/biz/services/rpc/adaptor.go
index 3965227..eec01bc 100644
--- a/biz/services/rpc/adaptor.go
+++ b/biz/services/rpc/adaptor.go
@@ -71,3 +71,16 @@ func peerCommonToIDL(p *common.Peer) *trunker.Peer {
 		Source:     trunker.Source(p.Source),
 	}
 }
+
+func sourceToMetrics(s trunker.Source) string {
+	source := "http"
+	switch s {
+	case trunker.Source_HTTP:
+		source = "http"
+	case trunker.Source_UDP:
+		source = "udp"
+	case trunker.Source_WS:
+		source = "ws"
+	}
+	return source
+}
diff --git a/biz/services/rpc/handler.go b/biz/services/rpc/handler.go
index d01f8b3..be50741 100644
--- a/biz/services/rpc/handler.go
+++ b/biz/services/rpc/handler.go
@@ -6,7 +6,9 @@ import (
 
 	"github.com/PBH-BTN/trunker/biz/services/peer"
 	"github.com/PBH-BTN/trunker/kitex_gen/pbh/btn/trunker"
+	"github.com/PBH-BTN/trunker/service/metrics"
 	"github.com/PBH-BTN/trunker/utils"
+	"github.com/PBH-BTN/trunker/utils/bittorrent"
 )
 
 // TrunkerServiceImpl implements the last service interface defined in the IDL.
@@ -26,6 +28,12 @@ func (s *TrunkerServiceImpl) Announce(ctx context.Context, request *trunker.Anno
 	if err != nil {
 		return nil, err
 	}
+	go func() {
+		metrics.EmitCounter(metrics.CounterAnnounce, 1, map[string]string{
+			metrics.LabelSource: sourceToMetrics(request.Source),
+			metrics.LabelClient: bittorrent.ParsePeerID(request.PeerId),
+		})
+	}()
 	return &trunker.AnnounceResponse{Peers: utils.Map(resp, peerCommonToIDL)}, nil
 }
 
diff --git a/conf/local.yaml b/conf/local.yaml
index dcff1bc..c30d8ed 100644
--- a/conf/local.yaml
+++ b/conf/local.yaml
@@ -1,4 +1,5 @@
 cache:
+  enable: false
   host: localhost
   port: 6379
 
@@ -11,7 +12,6 @@ tracker:
     persistFile: persist.dat
     maxPeersPerTorrent: 1500
     shard: 40
-    enableWS: true
   rpc:
     enableServer: false
     hostPorts: 0.0.0.0:6789
@@ -31,6 +31,10 @@ tracker:
   udpServer:
     enable: true
     hostPorts: :6881
+  wsServer:
+    enable: true
+    shard: 10
+  trackerId: default # https://wiki.theory.org/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol
 rocketmq:
   topic: TrunkerEvent
   endpoint: localhost:8081
\ No newline at end of file
diff --git a/conf/prod.yaml b/conf/prod.yaml
index 218e0fa..081c6c0 100644
--- a/conf/prod.yaml
+++ b/conf/prod.yaml
@@ -1,4 +1,5 @@
 cache:
+  enable: false
   host: localhost
   port: 6379
 
@@ -31,7 +32,10 @@ tracker:
   udpServer:
     enable: true
     hostPorts: 127.0.0.1:6881
-
+  wsServer:
+    enable: true
+    shard: 10
+  trackerId: default
 rocketmq:
   topic: TrunkerEvent
   endpoint: 127.0.0.1:8081
\ No newline at end of file
diff --git a/docs/config.adoc b/docs/config.adoc
index 889a11d..76afa20 100644
--- a/docs/config.adoc
+++ b/docs/config.adoc
@@ -16,6 +16,9 @@ The configuration file is in YAML format, you can find the example configuration
 |enableEventProducer |Send peer event to the mq. Caution: This will produce tons of message. |false
 |enableMetrics|Enable prometheus metrics|false
 |metricsHostPorts|The host and port of the metrics server|`127.0.0.1:9091`
+|udpServer| The config of the UDP server| false
+|wsServer| The config of the Websocket server| false
+|trackerId|A string that the client should send back on its next announcements. If absent and a previous announce sent a tracker id, do not discard the old value; keep using it.|`default`
 |===
 
 == Memory
@@ -32,8 +35,7 @@ If your mode is `memory`, you can use the following configuration.
 |===
 |Name |Description |Default 
 
-|useDB |Use the database to store the data. (currently no usable) |false 
-|enablePersist |Save the peers to the disk and load them while launching. |true 
+|enablePersist |Save the peers to the disk and load them while launching. |true
 |maxPeersPerTorrent |The max number of peers per torrent. |100 
 |shard |The number of shards. More shards will improve response time, by cost more memory. |16
 |enableWS|Enable Websocket tracker for WebTorrents|false
@@ -121,4 +123,26 @@ CAUTION: UDP server can't be protected with CDN, you should protect it by yourse
 
 == JSON Config
 
-JSON config is also supported with env `TRUNKER_CONFIG`, this will be useful for docker.
\ No newline at end of file
+JSON config is also supported with env `TRUNKER_CONFIG`, this will be useful for docker.
+
+== Other Config
+=== Cache
+Trunker support to use redis as cache, which is useful for scrape.
+The redis config is in the `cache` field.
+[cols="<,<,<"]
+|===
+|Name |Description |Default
+
+|enable |Enable redis cache|false
+|host |Host of redis |
+|port |Port of redis |
+|===
+
+=== Rocketmq
+Trunker can send the peer event to the rocketmq, which is useful for the analysis.
+The rocketmq config is in the `rocketmq` field.
+[cols="<,<,<"]
+|===
+|Name |Description |Default
+|topic| The topic of the rocketmq|*required*, no default
+|endpoint| The endpoint of the rocketmq|*required*, no default
\ No newline at end of file
diff --git a/go.mod b/go.mod
index fc7a071..80b6c3c 100755
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/PBH-BTN/trunker
 go 1.24
 
 require (
+	github.com/DataDog/zstd v1.5.6
 	github.com/apache/rocketmq-client-go/v2 v2.1.2
 	github.com/bits-and-blooms/bloom/v3 v3.7.0
 	github.com/bytedance/gopkg v0.1.1
@@ -19,7 +20,7 @@ require (
 	github.com/lestrrat-go/choose v0.0.0-20220106133416-f26c14db9cd7
 	github.com/panjf2000/gnet/v2 v2.7.2
 	github.com/prometheus/client_golang v1.21.1
-	github.com/redis/go-redis/v9 v9.7.0
+	github.com/redis/go-redis/v9 v9.7.1
 	github.com/shamaton/msgpack/v2 v2.2.2
 	github.com/stretchr/testify v1.10.0
 	github.com/thinkeridea/go-extend v1.3.2
@@ -38,19 +39,20 @@ require (
 require (
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/bits-and-blooms/bitset v1.21.0 // indirect
+	github.com/bits-and-blooms/bitset v1.22.0 // indirect
+	github.com/bufbuild/protocompile v0.14.1 // indirect
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
 	github.com/cloudwego/configmanager v0.2.2 // indirect
-	github.com/cloudwego/dynamicgo v0.5.2 // indirect
+	github.com/cloudwego/dynamicgo v0.6.1 // indirect
 	github.com/cloudwego/fastpb v0.0.5 // indirect
-	github.com/cloudwego/frugal v0.2.3 // indirect
+	github.com/cloudwego/frugal v0.2.4 // indirect
 	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/cloudwego/localsession v0.1.2 // indirect
 	github.com/cloudwego/netpoll v0.6.5 // indirect
 	github.com/cloudwego/runtimex v0.1.1 // indirect
-	github.com/cloudwego/thriftgo v0.3.18 // indirect
+	github.com/cloudwego/thriftgo v0.3.19 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
@@ -62,8 +64,8 @@ require (
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/iancoleman/strcase v0.2.0 // indirect
-	github.com/jhump/protoreflect v1.8.2 // indirect
+	github.com/iancoleman/strcase v0.3.0 // indirect
+	github.com/jhump/protoreflect v1.17.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -73,7 +75,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/nyaruka/phonenumbers v1.5.0 // indirect
-	github.com/panjf2000/ants/v2 v2.11.1 // indirect
+	github.com/panjf2000/ants/v2 v2.11.2 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -94,13 +96,12 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/arch v0.15.0 // indirect
-	golang.org/x/net v0.33.0 // indirect
+	golang.org/x/net v0.37.0 // indirect
 	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
-	google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
+	google.golang.org/grpc v1.70.0 // indirect
 	gopkg.in/eapache/queue.v1 v1.1.0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	stathat.com/c/consistent v1.0.0 // indirect
 )
-
-replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
diff --git a/go.sum b/go.sum
index 1ae418c..763d39e 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
 github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
+github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -19,8 +21,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
 github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
-github.com/bits-and-blooms/bitset v1.21.0 h1:9RlxRbMI5dRNNburKqfDSiz5POfImKgtablyV01WUw0=
-github.com/bits-and-blooms/bitset v1.21.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
+github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 github.com/bits-and-blooms/bloom/v3 v3.7.0 h1:VfknkqV4xI+PsaDIsoHueyxVDZrfvMn56jeWUzvzdls=
 github.com/bits-and-blooms/bloom/v3 v3.7.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@@ -29,6 +31,8 @@ github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdb
 github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
+github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
 github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM=
 github.com/bytedance/gopkg v0.0.0-20240507064146-197ded923ae3/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ=
 github.com/bytedance/gopkg v0.1.0/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ=
@@ -44,7 +48,6 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4
 github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
 github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
 github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -65,12 +68,12 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy
 github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
 github.com/cloudwego/configmanager v0.2.2 h1:sVrJB8gWYTlPV2OS3wcgJSO9F2/9Zbkmcm1Z7jempOU=
 github.com/cloudwego/configmanager v0.2.2/go.mod h1:ppiyU+5TPLonE8qMVi/pFQk2eL3Q4P7d4hbiNJn6jwI=
-github.com/cloudwego/dynamicgo v0.5.2 h1:hw4AUvaQP49TOI6hqIhyDd4N1nbaKTH3vOOgiaEftyU=
-github.com/cloudwego/dynamicgo v0.5.2/go.mod h1:DknfxjIMuGvXow409bS/AWycXONdc02HECBL0qpNqTY=
+github.com/cloudwego/dynamicgo v0.6.1 h1:hATTf0n7y4uiKXubYXIfhJp898PR02CJWP/0Wu6lAXM=
+github.com/cloudwego/dynamicgo v0.6.1/go.mod h1:ZfuIc4tsk8gdsmsoL+3M/q3916xTj+KAVJaXQHSaWiE=
 github.com/cloudwego/fastpb v0.0.5 h1:vYnBPsfbAtU5TVz5+f9UTlmSCixG9F9vRwaqE0mZPZU=
 github.com/cloudwego/fastpb v0.0.5/go.mod h1:Bho7aAKBUtT9RPD2cNVkTdx4yQumfSv3If7wYnm1izk=
-github.com/cloudwego/frugal v0.2.3 h1:t1hhhAi8lXcx7Ncs4PR1pSZ90vlDU1cy5K2btDMFpoA=
-github.com/cloudwego/frugal v0.2.3/go.mod h1:nC1U47gswLRiaxv6dybrhZvsDGCfQP9RGiiWC73CnoI=
+github.com/cloudwego/frugal v0.2.4 h1:C6LjbJJBiiz/b/SRpRv9DqfR5xB4/FlrIemsc4YhxfI=
+github.com/cloudwego/frugal v0.2.4/go.mod h1:nC1U47gswLRiaxv6dybrhZvsDGCfQP9RGiiWC73CnoI=
 github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
 github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
 github.com/cloudwego/hertz v0.9.4-0.20241021100040-3477b0309b81/go.mod h1:gGVUfJU/BOkJv/ZTzrw7FS7uy7171JeYIZvAyV3wS3o=
@@ -87,9 +90,8 @@ github.com/cloudwego/netpoll v0.6.5 h1:6E/BWhSzQoyLg9Kx/4xiMdIIpovzwBtXvuqSqaTUz
 github.com/cloudwego/netpoll v0.6.5/go.mod h1:BtM+GjKTdwKoC8IOzD08/+8eEn2gYoiNLipFca6BVXQ=
 github.com/cloudwego/runtimex v0.1.1 h1:lheZjFOyKpsq8TsGGfmX9/4O7F0TKpWmB8on83k7GE8=
 github.com/cloudwego/runtimex v0.1.1/go.mod h1:23vL/HGV0W8nSCHbe084AgEBdDV4rvXenEUMnUNvUd8=
-github.com/cloudwego/thriftgo v0.3.18 h1:gnr1vz7G3RbwwCK9AMKHZf63VYGa7ene6WbI9VrBJSw=
-github.com/cloudwego/thriftgo v0.3.18/go.mod h1:AdLEJJVGW/ZJYvkkYAZf5SaJH+pA3OyC801WSwqcBwI=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cloudwego/thriftgo v0.3.19 h1:G5W8sLFXjuip8Kwl+bv+am48WFiyCJIydN6AiYlwsoo=
+github.com/cloudwego/thriftgo v0.3.19/go.mod h1:AdLEJJVGW/ZJYvkkYAZf5SaJH+pA3OyC801WSwqcBwI=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -111,10 +113,6 @@ github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
 github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
@@ -166,7 +164,6 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -180,7 +177,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -190,15 +186,12 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
 github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -216,8 +209,8 @@ github.com/hertz-contrib/websocket v0.2.0/go.mod h1:+xUh5RJ1uaWiKKU5gKy+0iBw7Trc
 github.com/hitoshi44/go-uid64 v0.2.0 h1:BIznyxObE1qTfhQmVR1rj3vQQfwrGXB43usup+0b3Y8=
 github.com/hitoshi44/go-uid64 v0.2.0/go.mod h1:ayyZ6i5xUezXCJcRBk6LXtKUzPO/OgeTVE9Y5iwownw=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
-github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -230,8 +223,8 @@ github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiw
 github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
 github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jander/golog v0.0.0-20150917071935-954a5be801fc/go.mod h1:uWhWXOR4dpfk9J8fegnMY7sP2GFXxe3PFI9Ps+TRXJs=
-github.com/jhump/protoreflect v1.8.2 h1:k2xE7wcUomeqwY0LDCYA16y4WWfyTcMx5mKhk0d4ua0=
-github.com/jhump/protoreflect v1.8.2/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
+github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
+github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
 github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -294,7 +287,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
-github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
 github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
 github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -318,8 +310,8 @@ github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuK
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
-github.com/panjf2000/ants/v2 v2.11.1 h1:3FvycSRXomAF4mp9astbsibKh1Cnrk9w4c2nz99IZ50=
-github.com/panjf2000/ants/v2 v2.11.1/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
+github.com/panjf2000/ants/v2 v2.11.2 h1:AVGpMSePxUNpcLaBO34xuIgM1ZdKOiGnpxLXixLi5Jo=
+github.com/panjf2000/ants/v2 v2.11.2/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
 github.com/panjf2000/gnet/v2 v2.7.2 h1:c+QhXBKi/Qfdi4fh8ju6xiShGQHS1lHSEk6euFzJaIk=
 github.com/panjf2000/gnet/v2 v2.7.2/go.mod h1:PIMw/8ILZsN/4K11bqDtSE1rEVPoFtjFlc0Q4edkncA=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@@ -338,7 +330,6 @@ github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGC
 github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
@@ -350,10 +341,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
-github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
+github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
+github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -428,8 +418,6 @@ github.com/xxjwxc/gowp v0.0.0-20240929033016-5be68d222389/go.mod h1:oaLsbo1ZWr4j
 github.com/xxjwxc/public v0.0.0-20200603115833-341beff27850/go.mod h1:fp3M+FEQrCgWD1fZ/PLwZkCTglf086OEhC9LcydAUnc=
 github.com/xxjwxc/public v0.0.0-20210518123934-6cc0965f0bc5 h1:PYu1xvS3JoXfLl3Yz5H0vdxdlvsMItQoFOZccJMIG54=
 github.com/xxjwxc/public v0.0.0-20210518123934-6cc0965f0bc5/go.mod h1:za2pkqdDH64CbdyuZz6dqI+IhjCgstXeoWD3IAWbiAc=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -459,25 +447,18 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
 golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -486,33 +467,27 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -536,9 +511,6 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -559,7 +531,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -569,20 +540,13 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -592,27 +556,18 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384 h1:z+j74wi4yV+P7EtK9gPLGukOk7mFOy9wMQaC0wNb7eY=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
+google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
@@ -626,7 +581,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/eapache/queue.v1 v1.1.0 h1:EldqoJEGtXYiVCMRo2C9mePO2UUGnYn2+qLmlQSqPdc=
 gopkg.in/eapache/queue.v1 v1.1.0/go.mod h1:wNtmx1/O7kZSR9zNT1TTOJ7GLpm3Vn7srzlfylFbQwU=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/go-with/wxpay.v1 v1.3.0/go.mod h1:12lWy92n19pAUSSE3BrOiEZbWRkl+9tneOd/aU/LU6g=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
@@ -660,8 +614,6 @@ gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
 gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
 gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 stathat.com/c/consistent v1.0.0 h1:ezyc51EGcRPJUxfHGSgJjWzJdj3NiMU9pNfLNGiXV0c=
diff --git a/init.go b/init.go
index 2d302e6..c1aae32 100644
--- a/init.go
+++ b/init.go
@@ -6,8 +6,10 @@ import (
 
 	"github.com/PBH-BTN/trunker/biz/config"
 	"github.com/PBH-BTN/trunker/biz/services/peer"
+	"github.com/PBH-BTN/trunker/biz/services/peer/websocket"
 	"github.com/PBH-BTN/trunker/biz/services/rpc"
 	"github.com/PBH-BTN/trunker/biz/services/udp_server"
+	"github.com/PBH-BTN/trunker/service/cache"
 	"github.com/PBH-BTN/trunker/service/metrics"
 	"github.com/PBH-BTN/trunker/service/mq/producer"
 	"github.com/cloudwego/hertz/pkg/app/server"
@@ -22,6 +24,12 @@ func Init() {
 	if config.AppConfig.Tracker.EnableEventProducer {
 		producer.Init()
 	}
+	if config.AppConfig.Cache.Enable {
+		cache.Init()
+	}
+	if config.AppConfig.Tracker.WSServer.Enable {
+		websocket.InitWSMuxLocalManager()
+	}
 	metrics.Init()
 	//cache.Init()
 	peer.InitPeerManager()
diff --git a/service/cache/utils.go b/service/cache/utils.go
index fdc4d13..bb68714 100644
--- a/service/cache/utils.go
+++ b/service/cache/utils.go
@@ -20,7 +20,6 @@ func Get[T any](ctx context.Context, key string) (*T, bool) {
 		hlog.CtxWarnf(ctx, "failed to unmarshal value, key: %s, value: %s , error:%s", key, str, err)
 		return nil, false
 	}
-	hlog.CtxDebugf(ctx, "get value from redis, key: "+key)
 	return &value, true
 }
 
@@ -35,7 +34,6 @@ func GetList[T any](ctx context.Context, key string) ([]*T, bool) {
 		hlog.CtxWarnf(ctx, "failed to unmarshal value, key: %s, value: %s , error:%s", key, str, err)
 		return nil, false
 	}
-	hlog.CtxDebugf(ctx, "get value from redis, key: "+key)
 	return value, true
 }
 
diff --git a/service/metrics/torrent.go b/service/metrics/torrent.go
index 183f73f..2d30bca 100644
--- a/service/metrics/torrent.go
+++ b/service/metrics/torrent.go
@@ -2,6 +2,7 @@ package metrics
 
 import (
 	"context"
+	"sync/atomic"
 
 	"github.com/PBH-BTN/trunker/biz/services/peer"
 	"github.com/PBH-BTN/trunker/biz/services/peer/common"
@@ -16,6 +17,7 @@ const (
 )
 
 var info *common.StatisticInfo
+var fetchCount uint32 = 0 // fetch is a very expensive operation, so we only do it every 10 times
 
 func gaugeSet(gaugeVec *prometheus.GaugeVec, value uint64, labels prometheus.Labels) error {
 	gauge, err := gaugeVec.GetMetricWith(labels)
@@ -43,9 +45,14 @@ func registerGauge(registry *prometheus.Registry) map[counterMetrics]prometheus.
 		Help: "Total Torrent numbers",
 	}, func() float64 {
 		var err error
-		info, err = peer.GetPeerManager().GetStatistic(context.Background())
-		if err != nil {
-			return 0
+		if info == nil || fetchCount == 20 {
+			info, err = peer.GetPeerManager().GetStatistic(context.Background())
+			if err != nil {
+				return 0
+			}
+			atomic.StoreUint32(&fetchCount, 0)
+		} else {
+			atomic.AddUint32(&fetchCount, 1)
 		}
 		for s, v := range info.Shards {
 			_ = gaugeSet(m[GaugePeer].(*prometheus.GaugeVec), v.TotalPeers, prometheus.Labels{LabelShards: s})
diff --git a/utils/bencode/bencode.go b/utils/bencode/bencode.go
index fe0fca6..86e05af 100644
--- a/utils/bencode/bencode.go
+++ b/utils/bencode/bencode.go
@@ -1,6 +1,7 @@
 package bencode
 
 import (
+	"github.com/PBH-BTN/trunker/biz/config"
 	"github.com/PBH-BTN/trunker/biz/model"
 	"github.com/cloudwego/hertz/pkg/app"
 )
@@ -10,5 +11,5 @@ func ResponseOk(c *app.RequestContext, data any) {
 }
 
 func ResponseErr(c *app.RequestContext, err error) {
-	c.Render(400, BencodeRender{model.ErrorResponse{FailureReason: err.Error(), Retry: "never"}})
+	c.Render(400, BencodeRender{model.ErrorResponse{FailureReason: err.Error(), Retry: "never", TrackerId: config.AppConfig.Tracker.TrackerId}})
 }
diff --git a/utils/conv/string.go b/utils/conv/string.go
index d03a3e6..43ebb85 100644
--- a/utils/conv/string.go
+++ b/utils/conv/string.go
@@ -1,13 +1,24 @@
 package conv
 
-import "golang.org/x/text/encoding/charmap"
+import (
+	"encoding/hex"
+
+	"github.com/cloudwego/hertz/pkg/common/hlog"
+	"golang.org/x/text/encoding/charmap"
+)
 
 func TransUTF8To8859_1(raw []byte) []byte {
-	encoded, _ := charmap.ISO8859_1.NewEncoder().Bytes(raw)
+	encoded, err := charmap.ISO8859_1.NewEncoder().Bytes(raw)
+	if err != nil {
+		hlog.Warnf("failed to convert utf8 to 8859-1:%s,raw:%s", err.Error(), hex.EncodeToString(raw))
+	}
 	return encoded
 }
 
 func Trans8859_1ToUTF8(raw []byte) []byte {
-	encoded, _ := charmap.ISO8859_1.NewDecoder().Bytes(raw)
+	encoded, err := charmap.ISO8859_1.NewDecoder().Bytes(raw)
+	if err != nil {
+		hlog.Warnf("failed to convert 8859-1 to utf8:%s,raw:%s", err.Error(), hex.EncodeToString(raw))
+	}
 	return encoded
 }


Reviews:
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-03-13T03:50:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>biz/services/peer/local/local.go (1)</summary><blockquote>

`300-306`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**GetPeers方法可能与新的数据结构不兼容**

`GetPeers` 方法调用 `utils.SkipMapToSlice(peerMap)`，但不清楚这个实用函数是否能够正确处理新的基于数组的映射结构。

验证 `utils.SkipMapToSlice` 函数能否正确处理修改后的 `InfoHashRoot` 结构：

---

🏁 Script executed:

```shell
#!/bin/bash
# 检查 SkipMapToSlice 函数的实现以及它与新的 InfoHashRoot 结构的兼容性
rg "func SkipMapToSlice" utils/
cat utils/$(rg -l "func SkipMapToSlice" utils/)
```

Length of output: 247

---

🏁 Script executed:

```shell

#!/bin/bash
# 重新验证 utils/skipmap.go 中 SkipMapToSlice 的实现，
# 确保其能兼容新的 InfoHashRoot 数据结构。
file=$(rg -l "func SkipMapToSlice" utils/)
echo "找到文件: $file"
cat "$file"
```

Length of output: 621

---

🏁 Script executed:

```shell

#!/bin/bash
# 搜索代码库中 InfoHashRoot 结构的定义以验证其接口实现
rg "type InfoHashRoot" .
```

Length of output: 210

---

🏁 Script executed:

```shell

#!/bin/bash
rg "type InfoHashRoot struct" -A 20 biz/services/peer/local/local.go
```

Length of output: 791

---



</details>

**修正建议：请更新 GetPeers 方法以正确适配 InfoHashRoot 新结构的设计**

目前 `GetPeers` 方法直接将从 `m.infoHashMap.Load(infoHash)` 读取的值传递给 `utils.SkipMapToSlice`。但根据验证，新的数据结构 `InfoHashRoot` 定义中其 `peerMap` 字段是一个长度为 3 的数组，每个元素都是一个 `mapx.SyncStringMap[*common.Peer]`。而 `utils.SkipMapToSlice` 仅适用于实现了 `rangeAble` 接口（即具备 `Range` 和 `Len` 方法）的单个 map 类型。

- **问题概述：**  
  - 当前代码的 `peerMap` 可能直接引用了 `InfoHashRoot` 实例，而其类型为 `[3]mapx.SyncStringMap[*common.Peer]` 并不实现 `rangeAble` 接口。  
  - 这意味着直接调用 `utils.SkipMapToSlice(peerMap)` 可能会导致运行时错误或者不按预期返回所有 peer。

- **建议方案：**  
  - 如果预期的是从 `InfoHashRoot` 中整合所有三个 map 的数据，建议在 `GetPeers` 方法中遍历 `InfoHashRoot.peerMap` 数组，对每个内部 map 分别调用 `utils.SkipMapToSlice`，并将结果合并后返回。  
  - 或者，可考虑让 `InfoHashRoot` 自身实现 `rangeAble` 接口方法（例如，通过遍历所有内部 map 实现 `Range` 和 `Len`），以兼容现有的 `SkipMapToSlice` 调用逻辑。

请开发者根据预期的业务逻辑调整相关代码，确保从新结构中能够正确获取并返回 peers。

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>service/metrics/torrent.go (1)</summary><blockquote>

`47-55`: **更新注释以匹配实际行为**

注释中说明每10次操作执行一次获取，但代码中实际是每20次才获取一次。

更新注释以匹配实际行为：

```diff
-var fetchCount int = 0 // fetch is a very expensive operation, so we only do it every 10 times
+var fetchCount int = 0 // fetch is a very expensive operation, so we only do it every 20 times
```

</blockquote></details>
<details>
<summary>biz/services/peer/local/local.go (1)</summary><blockquote>

`164-165`: **Range方法的使用可能导致重复处理**

`Range` 方法遍历所有三个映射，如果同一个对等点存在于多个映射中，会被多次处理。有必要实现某种形式的去重逻辑，确保每个对等点只被处理一次。

考虑在 `Range` 方法中使用集合来跟踪已处理的键：

```diff
 func (i *InfoHashRoot) Range(f func(key string, value *common.Peer) bool) {
+    processedKeys := make(map[string]bool)
     for _, s := range i.peerMap {
-        s.Range(f)
+        s.Range(func(key string, value *common.Peer) bool {
+            if _, processed := processedKeys[key]; !processed {
+                processedKeys[key] = true
+                return f(key, value)
+            }
+            return true
+        })
     }
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f09fb458c7fdff2f483624e5f07ceeb3a7622a1c and cfa2ee7850c1d34d8982f3eafd34e0d7c75da2f6.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `biz/services/peer/local/clean.go` (2 hunks)
* `biz/services/peer/local/local.go` (10 hunks)
* `service/metrics/torrent.go` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>biz/services/peer/local/clean.go (2)</summary>

`34-44`: **优化了过期时间计算并适应了新的数据结构**

将过期时间计算从循环内移到循环外是一个很好的优化，减少了重复计算。代码现在使用新的 `root.Range` 方法来遍历所有三个映射，使清理过程更加完整。

---

`49-51`: **检查空映射使用了新方法**

代码现在正确地使用 `root.Len()` 而不是 `root.peerMap.Len()` 来检查所有三个映射是否都为空，这与新的数据结构相符。

</details>
<details>
<summary>biz/services/peer/local/local.go (3)</summary>

`26-31`: **环形缓冲区设计实现**

采用了三个映射的环形缓冲区设计，一个用于写入，一个用于只读，一个保持为空，这是一个优秀的设计，可以提高内存管理效率并减少全局锁定需求。

---

`46-53`: **Load方法可能导致重复计数**

当对等点信息在转换期间存在于多个映射中时，这种实现无法处理去重逻辑。对于统计或计数操作（如 `Scrape` 方法）来说，这可能导致重复计数问题。

考虑添加一个可选参数，控制是否在找到第一个匹配项后停止遍历：

```diff
-func (i *InfoHashRoot) Load(key string) (*common.Peer, bool) {
+func (i *InfoHashRoot) Load(key string, stopOnFirst ...bool) (*common.Peer, bool) {
+    shouldStop := len(stopOnFirst) > 0 && stopOnFirst[0]
     for _, peerMap := range i.peerMap {
         if v, ok := peerMap.Load(key); ok {
+            if shouldStop {
                 return v, ok
+            }
+            // 可以继续搜索并返回最新版本或聚合结果
         }
     }
     return nil, false
 }
```

---

`313-337`: **AnswerToPeer方法使用了新的 Load 方法**

该方法现在正确地使用 `root.Load` 而不是 `root.peerMap.Load`，与新的数据结构保持一致。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-13T09:55:53Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-03-17T11:08:29Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-03-17T11:08:30Z

Review comments:
diff_hunk: @@ -16,6 +16,7 @@ const (
 )
 
 var info *common.StatisticInfo
+var fetchCount int = 0 // fetch is a very expensive operation, so we only do it every 10 times
path: service/metrics/torrent.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**注意潜在的竞态条件**

`fetchCount` 是一个全局变量，没有使用互斥锁或原子操作来保护，在多个 goroutines 同时访问时可能导致竞态条件。

建议使用原子操作来更新和读取这个变量：

```diff
-var fetchCount int = 0 // fetch is a very expensive operation, so we only do it every 10 times
+var fetchCount atomic.Int32 // fetch is a very expensive operation, so we only do it every 20 times
```

然后在相应的地方使用原子操作进行读写：

```diff
-if info == nil || fetchCount == 20 {
+if info == nil || fetchCount.Load() == 20 {
  ...
-  fetchCount = 0
+  fetchCount.Store(0)
} else {
-  fetchCount++
+  fetchCount.Add(1)
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
package metrics

import (
    "sync/atomic"
)

// Global variable declaration updated to use atomic operations.
var fetchCount atomic.Int32 // fetch is a very expensive operation, so we only do it every 20 times

// Example function that uses fetchCount.
func processMetrics(info interface{}) {
    if info == nil || fetchCount.Load() == 20 {
        // ... perform the expensive operation
        fetchCount.Store(0)
    } else {
        fetchCount.Add(1)
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T09:55:51Z
author_association: NONE
diff_hunk: @@ -42,10 +43,10 @@ func (m *Manager) cleanUp(root *InfoHashRoot) int64 {
 		return true
 	})
 	for _, key := range toClean {
-		root.peerMap.Delete(key)
+		root.LoadAndDelete(key)
 	}
path: biz/services/peer/local/clean.go
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**加载并删除逻辑可能不完整**

当前的 `LoadAndDelete` 实现（在 local.go 中）会在找到第一个匹配项后立即返回，而不会检查其他映射中的重复项。这在转换期间可能导致一些对等点在某些映射中保留下来。

请验证清理过程是否能有效地从所有三个映射中删除过期的对等点。如果不能，考虑修改 `LoadAndDelete` 方法或在这里使用一个遍历所有映射的循环。

---

🏁 Script executed:

```shell
#!/bin/bash
# 检查 LoadAndDelete 是否可能在某些映射中保留重复项
rg -A 15 "func \(i \*InfoHashRoot\) LoadAndDelete" biz/services/peer/local/
```

Length of output: 1065

---



</details>

**修复建议：LoadAndDelete 方法仅删除第一个匹配项**

- 在 `biz/services/peer/local/local.go` 中的 `LoadAndDelete` 方法发现，它会对所有映射进行遍历，但一旦在某个映射中找到匹配项就立即返回，未能继续删除其他映射中可能存在的重复项。
- 这种行为可能导致在清理过程中，只能删除部分过期对等点，而其他映射中的重复项会被遗漏。
  
请评估是否需要：
- 修改 `LoadAndDelete` 方法，使其能遍历所有映射并彻底删除相同 key 的条目，或
- 在调用处（如 `biz/services/peer/local/clean.go` 中）采用额外逻辑来确保所有映射中的相应对等点均被清理。

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T09:55:52Z
author_association: NONE
diff_hunk: @@ -24,19 +24,59 @@ import (
 )
 
 type InfoHashRoot struct {
-	peerMap   mapx.SyncStringMap[*common.Peer]
-	lastClean time.Time
-	infoHash  string
+	peerMap       [3]mapx.SyncStringMap[*common.Peer] // always keep 3 map, one for write, one for readonly and one keeps empty
+	lastClean     time.Time
+	infoHash      string
+	currentActive uint32
 }
 
 func NewInfoHashRoot(infoHash string) *InfoHashRoot {
 	return &InfoHashRoot{
-		peerMap:   mapx.NewSkipMap[*common.Peer](),
+		currentActive: 0,
+		peerMap: [3]mapx.SyncStringMap[*common.Peer]{
+			mapx.NewSkipMap[*common.Peer](),
+			mapx.NewSkipMap[*common.Peer](),
+			mapx.NewSkipMap[*common.Peer](),
+		},
 		lastClean: time.Now(),
 		infoHash:  infoHash,
 	}
 }
 
+func (i *InfoHashRoot) Load(key string) (*common.Peer, bool) {
+	for _, peerMap := range i.peerMap {
+		if v, ok := peerMap.Load(key); ok {
+			return v, ok
+		}
+	}
+	return nil, false
+}
+
+func (i *InfoHashRoot) LoadAndDelete(key string) (*common.Peer, bool) {
+	for _, peerMap := range i.peerMap {
+		if v, ok := peerMap.LoadAndDelete(key); ok {
+			return v, ok
+		}
+	}
+	return nil, false
+}
path: biz/services/peer/local/local.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**LoadAndDelete可能留下重复项**

当前的实现只从找到的第一个映射中删除条目，如果对等点存在于多个映射中（例如，在转换期间），其他实例可能会保留下来，导致数据不一致。

修改为从所有映射中删除相应的对等点：

```diff
 func (i *InfoHashRoot) LoadAndDelete(key string) (*common.Peer, bool) {
+    var foundPeer *common.Peer
+    var found bool
+    
     for _, peerMap := range i.peerMap {
         if v, ok := peerMap.LoadAndDelete(key); ok {
-            return v, ok
+            foundPeer = v
+            found = true
+            // 不要立即返回，继续删除其他映射中的副本
         }
     }
-    return nil, false
+    return foundPeer, found
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
func (i *InfoHashRoot) LoadAndDelete(key string) (*common.Peer, bool) {
    var foundPeer *common.Peer
    var found bool
    
    for _, peerMap := range i.peerMap {
        if v, ok := peerMap.LoadAndDelete(key); ok {
            foundPeer = v
            found = true
            // 不要立即返回，继续删除其他映射中的副本
        }
    }
    return foundPeer, found
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T09:55:52Z
author_association: NONE
diff_hunk: @@ -186,25 +209,28 @@
 	}
 	var complete, incomplete, downloaded, seeder atomic.Int64
 	p := workpool.New(runtime.NumCPU() * 2)
-	root.peerMap.Range(func(_ string, value *common.Peer) bool {
-		p.Do(func() error {
-			if value.Left == 0 {
-				downloaded.Add(1)
-				complete.Add(1)
-				if value.Event != common.PeerEvent_Stopped {
-					seeder.Add(1)
+	for _, s := range root.peerMap {
+		s.Range(func(_ string, value *common.Peer) bool {
+			p.Do(func() error {
+				if value.Left == 0 {
+					downloaded.Add(1)
+					complete.Add(1)
+					if value.Event != common.PeerEvent_Stopped {
+						seeder.Add(1)
+					}
+					return nil
+				}
+				if value.Event == common.PeerEvent_Completed {
+					complete.Add(1)
+				} else {
+					incomplete.Add(1)
 				}
 				return nil
-			}
-			if value.Event == common.PeerEvent_Completed {
-				complete.Add(1)
-			} else {
-				incomplete.Add(1)
-			}
-			return nil
+			})
+			return true
 		})
-		return true
-	})
+	}
+
path: biz/services/peer/local/local.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Scrape方法可能导致重复计数**

由于同一个对等点可能存在于多个映射中（尤其是在转换期间），`Scrape` 方法可能会多次计算同一个对等点，从而导致统计数据不准确。

建议使用集合来跟踪已处理的对等点：

```diff
 func (m *Manager) Scrape(ctx context.Context, infoHash string) (*model.ScrapeFile, error) {
     // ... 现有代码 ...
     var complete, incomplete, downloaded, seeder atomic.Int64
     p := workpool.New(runtime.NumCPU() * 2)
+    processedPeers := make(map[string]bool)
     for _, s := range root.peerMap {
         s.Range(func(_ string, value *common.Peer) bool {
+            if _, processed := processedPeers[value.GetKey()]; processed {
+                return true  // 跳过已处理的对等点
+            }
+            processedPeers[value.GetKey()] = true
             p.Do(func() error {
                 // ... 原有的计数逻辑 ...
                 return nil
             })
             return true
         })
     }
     // ... 现有代码 ...
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    p := workpool.New(runtime.NumCPU() * 2)
    processedPeers := make(map[string]bool)
    for _, s := range root.peerMap {
        s.Range(func(_ string, value *common.Peer) bool {
            if _, processed := processedPeers[value.GetKey()]; processed {
                return true  // 跳过已处理的对等点
            }
            processedPeers[value.GetKey()] = true
            p.Do(func() error {
                if value.Left == 0 {
                    downloaded.Add(1)
                    complete.Add(1)
                    if value.Event != common.PeerEvent_Stopped {
                        seeder.Add(1)
                    }
                    return nil
                }
                if value.Event == common.PeerEvent_Completed {
                    complete.Add(1)
                } else {
                    incomplete.Add(1)
                }
                return nil
            })
            return true
        })
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T09:55:52Z
author_association: NONE
diff_hunk: @@ -158,19 +183,17 @@
 		resp = append(resp, value)
 		return true
 	})
-	if len(timeoutPeer) > 0 {
-		gopool.CtxGo(ctx, func() {
-			for _, toClean := range timeoutPeer {
-				root.peerMap.Delete(toClean.GetKey())
-			}
-		})
-	}
-	if shouldEject && oldestPeer != nil {
-		gopool.CtxGo(ctx, func() {
-			hlog.CtxDebugf(ctx, "info hash %s ejected %s:%d(%s) %s, last seen:%s", hex.EncodeToString(conv.UnsafeStringToBytes(root.infoHash)), oldestPeer.GetIP().String(), oldestPeer.Port, oldestPeer.ID, oldestPeer.UserAgent, oldestTime.Format(time.DateTime))
-			root.peerMap.Delete(oldestPeer.GetKey())
-		})
+	if root.peerMap[root.currentActive].Len() > config.AppConfig.Tracker.Memory.MaxPeersPerTorrent/2 { // reach max, start to eject
+		hlog.CtxDebugf(ctx, "[info_hash %s]active set full, currentActive %d, size: 0: %d 1:%d 2:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), root.currentActive, root.peerMap[0].Len(), root.peerMap[1].Len(), root.peerMap[2].Len())
+		current := root.currentActive
+		if atomic.CompareAndSwapUint32(&root.currentActive, current, (current+1)%3) { // write head switch to next
+			hlog.CtxDebugf(ctx, "[info_hash %s] active set swapped! current:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), root.currentActive)
+			// empty the oldest map
+			hlog.CtxDebugf(ctx, "[info_hash %s] clean oldest set %d, len:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), (current+2)%3, root.peerMap[(current+2)%3].Len())
+			root.peerMap[(current+2)%3] = mapx.NewSkipMap[*common.Peer]()
+		}
 	}
path: biz/services/peer/local/local.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**在切换活动映射时存在潜在的竞态条件**

当切换活动映射并清空最旧的映射时，没有适当的同步机制来保护清空操作，这可能在并发环境中导致问题。

建议使用互斥锁来保护映射切换和清空操作：

```diff
+    var switchLock sync.Mutex
     if root.peerMap[root.currentActive].Len() > config.AppConfig.Tracker.Memory.MaxPeersPerTorrent/2 { // reach max, start to eject
         hlog.CtxDebugf(ctx, "[info_hash %s]active set full, currentActive %d, size: 0: %d 1:%d 2:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), root.currentActive, root.peerMap[0].Len(), root.peerMap[1].Len(), root.peerMap[2].Len())
         current := root.currentActive
+        switchLock.Lock()
+        defer switchLock.Unlock()
         if atomic.CompareAndSwapUint32(&root.currentActive, current, (current+1)%3) { // write head switch to next
             hlog.CtxDebugf(ctx, "[info_hash %s] active set swapped! current:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), root.currentActive)
             // empty the oldest map
             hlog.CtxDebugf(ctx, "[info_hash %s] clean oldest set %d, len:%d", hex.EncodeToString(conv.UnsafeStringToBytes(req.InfoHash)), (current+2)%3, root.peerMap[(current+2)%3].Len())
             root.peerMap[(current+2)%3] = mapx.NewSkipMap[*common.Peer]()
         }
     }
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T09:55:52Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-13T03:47:33Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
此次改动调整了对 peer 数据管理的实现方式。原先直接操作单一的 peerMap 被替换为对 InfoHashRoot 内部三个同步映射的操作，并新增了多个操作方法（如 Load、LoadAndDelete、LoadOrStore、Store、Len 和 Range）。同时，Manager 中的 cleanUp 与 HandleAnnouncePeer 方法已更新，改为通过这些新方法进行 peer 的遍历、存取和删除，进一步改善了并发访问和内存管理。此外，StoreToPersist 方法的文件处理逻辑也进行了增强，采用临时文件策略以提高错误处理能力。

## Changes

| 文件                          | 变更摘要                                                                                                                               |
|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| biz/services/peer/local/clean.go | cleanUp 方法内部调整：用 InfoHashRoot 的 Range 遍历 peers、通过 LoadAndDelete 删除 peer，以及用 Len 判断集合是否为空。                  |
| biz/services/peer/local/local.go | 扩展 InfoHashRoot：用三个 SyncStringMap 管理 peers，新增 Load、LoadAndDelete、LoadOrStore、Store、Len 方法，并更新 Range 的遍历逻辑。 |
| biz/services/peer/mux_local/persist.go | StoreToPersist 方法重构：引入临时文件处理、增强错误处理和锁机制。                                                                      |
| service/metrics/torrent.go    | 新增 fetchCount 变量以控制获取统计信息的频率，修改 registerGauge 函数以每 20 次调用获取一次统计信息。                               |
| go.mod                        | 移除对 github.com/apache/thrift 的替换指令，允许模块解析到最新兼容版本。                                                             |
| biz/config/init.go            | RedisConfig 结构体新增 Enable 字段，表示 Redis 配置是否启用。                                                                        |
| conf/local.yaml               | cache 部分新增 enable: false 配置项，默认禁用缓存。                                                                                   |
| conf/prod.yaml                | cache 部分新增 enable: false 配置项，默认禁用缓存。                                                                                   |
| docs/config.adoc              | 新增配置文档部分，包含 Cache 和 Rocketmq 的配置选项说明。                                                                             |
| init.go                       | Init 函数中新增缓存服务初始化步骤，基于配置判断是否启用缓存。                                                                        |
| biz/handler/webtorrent.go     | 修改 handleWSScrape 函数，简化 infoHashes 的填充逻辑，移除对字符串的编码转换。                                                        |
| biz/handler/announce.go       | Announce 函数的响应结构中新增 TrackerId 字段，以便在响应中提供更多信息。                                                              |
| biz/model/http.go             | AnnounceBasicResponse 结构体中新增 TrackerId 字段，扩展数据结构以包含 tracker 的标识符。                                               |
| biz/services/peer/manager.go  | 移除 PeerManager 接口中的 AnswerToPeer 方法，表示不再支持通过该接口发送消息。                                                          |
| biz/services/peer/mux_local/mux_manager.go | 移除 MuxLocalManager 结构体中的 AnswerToPeer 方法，影响处理对等体请求的功能。                                                         |
| biz/services/peer/websocket/local.go | 新增 local.go 文件，定义 WebSocket 连接管理的 manager 结构体，包含处理 peer 连接的多个方法。                                          |
| biz/services/peer/websocket/manager.go | 新增 WebsocketMuxManager 结构体及其相关方法，用于管理 WebSocket 连接。                                                              |
| utils/bencode/bencode.go      | 修改 ResponseErr 函数以包含 TrackerId 字段，增强错误响应的信息。                                                                       |
| biz/services/rpc/adaptor.go   | 新增 sourceToMetrics 函数，将 trunker.Source 转换为字符串表示。                                                                        |
| biz/services/rpc/handler.go    | Announce 方法中新增 goroutine，用于异步跟踪 announce 事件的指标。                                                                       |
| service/cache/utils.go        | 移除 Get 和 GetList 函数中的调试日志语句，保持功能不变。                                                                                |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant M as Manager
    participant R as InfoHashRoot
    participant P as Peer集合

    M->>R: 调用 cleanUp(root)
    R->>R: 使用 Range 遍历所有 peer
    loop 对每个 peer
        R->>R: 调用 LoadAndDelete(key) 删除 peer
    end
    R-->>M: 返回删除的 peer 数量
```

```mermaid
sequenceDiagram
    participant M as Manager
    participant R as InfoHashRoot

    M->>R: 调用 HandleAnnouncePeer(peer)
    alt peer 已存在
        R-->>M: 通过 Load 更新现有 peer
    else
        R-->>M: 通过 Store 或 LoadOrStore 添加新 peer
    end
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Free**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9577b564e5df59ef1986493973fc424b48c35c84 and 859f2029f7bc11ce634ccf43dd715e3c9acc91db.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `biz/handler/webtorrent.go` (7 hunks)
* `biz/services/peer/mux_local/persist.go` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* biz/services/peer/mux_local/persist.go
* biz/handler/webtorrent.go

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

> [!NOTE]
> <details>
> <summary>🎁 Summarized by CodeRabbit Free</summary>
> 
> Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting <https://app.coderabbit.ai/login>.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.
> 
> </details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PBH-BTN/trunker&utm_content=21):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: Gaojianli
created_at: 2025-03-13T09:49:07Z
body: @coderabbitai check if there are any bugs
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-03-13T09:51:28Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>🧩 Analysis chain</summary>

I'll examine the code changes to look for potential bugs. First, let me fetch the modified files to understand the implementation details.

---

🏁 Script executed:
```
#!/bin/bash
# Get the modified files in the PR
git show --name-only refs/pull/21/head

# Get the content of the modified files
echo -e "\n--- biz/services/peer/local/clean.go ---"
git show refs/pull/21/head:biz/services/peer/local/clean.go

echo -e "\n--- biz/services/peer/local/local.go ---"
git show refs/pull/21/head:biz/services/peer/local/local.go

echo -e "\n--- service/metrics/torrent.go ---"
git show refs/pull/21/head:service/metrics/torrent.go
```

Length of output: 841

---

Let me try a different approach to get the files from the PR.

---

🏁 Script executed:
```
#!/bin/bash
# First, fetch the PR branch
git fetch origin pull/21/head:pr-21
git checkout pr-21

# Now list the modified files
git diff --name-only HEAD^

# Examine each file's content
echo -e "\n--- biz/services/peer/local/clean.go ---"
cat biz/services/peer/local/clean.go

echo -e "\n--- biz/services/peer/local/local.go ---"
cat biz/services/peer/local/local.go

echo -e "\n--- service/metrics/torrent.go ---"
cat service/metrics/torrent.go
```

Length of output: 13476

---

@Gaojianli After reviewing the code changes, I've identified several potential bugs:

1. **Race condition in metrics**: `fetchCount` in `service/metrics/torrent.go` is not protected against concurrent access. It should use atomic operations to prevent race conditions.

2. **Possible duplicate counting**: The `Scrape` method iterates over all three maps. If a peer exists in multiple maps during transition, it might be counted multiple times, inflating statistics.

3. **Potential map incompatibility**: The `GetPeers` method calls `utils.SkipMapToSlice(peerMap)` but it's unclear if this utility function properly handles the new array-based map structure.

4. **Selective deletion**: `LoadAndDelete` only removes an entry from the first map it's found in. If a peer exists in multiple maps (which could happen during rotation), some instances might remain.

Let me check the `utils.SkipMapToSlice` implementation to provide more details.

---

🏁 Script executed:
```
#!/bin/bash
# Check the SkipMapToSlice implementation
cat utils/SkipMapToSlice.go 2>/dev/null || find . -type f -name "*.go" | xargs grep -l "SkipMapToSlice"
grep -r "func SkipMapToSlice" --include="*.go" .
```

Length of output: 289

---

Let me examine the implementation of `SkipMapToSlice` to better understand if it's compatible with the new structure.

---

🏁 Script executed:
```
#!/bin/bash
# Check the SkipMapToSlice function and its usage
cat utils/skipmap.go
```

Length of output: 382

---

Let me examine how `GetPeers` works with the new structure.

---

🏁 Script executed:
```
#!/bin/bash
# Find and check the details of GetPeers implementation in the PR branch
grep -A 15 "GetPeers" biz/services/peer/local/local.go
# Check how InfoHashRoot.Range is used in other places
grep -r "utils\.SkipMapToSlice" --include="*.go" .
```

Length of output: 696

---

Let me check what `rangeAble` requires and if `InfoHashRoot` satisfies it for the `SkipMapToSlice` function:

---

🏁 Script executed:
```
#!/bin/bash
# Let's check how the interface is implemented
grep -A 20 "type rangeAble" utils/skipmap.go

# Check the implementation of Range and Len methods in InfoHashRoot
grep -A 10 "func (i \*InfoHashRoot) Range" biz/services/peer/local/local.go
grep -A 10 "func (i \*InfoHashRoot) Len" biz/services/peer/local/local.go

# Check for any test coverage of these functions
find . -name "*_test.go" | xargs grep -l "GetPeers\|SkipMapToSlice"
```

Length of output: 1004

---

Let me look at how the `InfoHashRoot` and `utils.SkipMapToSlice` interact:

---

🏁 Script executed:
```
#!/bin/bash
# Find how GetPeers is used in the codebase
grep -r "GetPeers" --include="*.go" . | grep -v "func"
```

Length of output: 703

---



</details>

@Gaojianli After thorough code review, I've found several potential bugs in the PR:

1. **Bug in `GetPeers` method**: It passes the entire `InfoHashRoot` to `utils.SkipMapToSlice`, but the `InfoHashRoot.Range` method iterates through all three maps which could return duplicate peers if a peer exists in multiple maps during rotation.

2. **Race condition in metrics**: `fetchCount` in `service/metrics/torrent.go` is modified without atomic operations, which could lead to race conditions in concurrent environments.

3. **Incomplete deletion in `LoadAndDelete`**: This method only removes the key from the first map it's found in and returns immediately. If a peer exists in multiple maps during rotation, it will remain in the other maps.

4. **Duplicate counting in `Scrape`**: The method iterates over all three maps and might count the same peer multiple times if it exists in multiple maps.

5. **Map clearing race condition**: When switching the active map, the code clears the oldest map with `root.peerMap[(current+2)%3] = mapx.NewSkipMap[*common.Peer]()` without proper synchronization for concurrent access to that map.

These issues should be addressed to ensure proper functionality of the ring buffer implementation.

<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE
user: login: Gaojianli
created_at: 2025-03-17T11:08:04Z
body: @coderabbitai  resolve this
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-03-17T11:08:27Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
@Gaojianli I'll resolve all the review comments as requested.

<details>
<summary>:white_check_mark: Actions performed</summary>

Comments resolved and changes approved.

</details>
author_association: NONE