github.com/TeaOSLab/EdgeNode@v1.3.8/internal/stats/dau_manager.go (about) 1 // Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . 2 3 package stats 4 5 import ( 6 "encoding/json" 7 "github.com/TeaOSLab/EdgeNode/internal/events" 8 "github.com/TeaOSLab/EdgeNode/internal/goman" 9 "github.com/TeaOSLab/EdgeNode/internal/remotelogs" 10 "github.com/TeaOSLab/EdgeNode/internal/trackers" 11 "github.com/TeaOSLab/EdgeNode/internal/utils/fasttime" 12 "github.com/TeaOSLab/EdgeNode/internal/utils/idles" 13 "github.com/TeaOSLab/EdgeNode/internal/utils/kvstore" 14 "github.com/iwind/TeaGo/Tea" 15 "github.com/iwind/TeaGo/types" 16 timeutil "github.com/iwind/TeaGo/utils/time" 17 "os" 18 "runtime" 19 "strings" 20 "sync" 21 "testing" 22 "time" 23 ) 24 25 var SharedDAUManager = NewDAUManager() 26 27 type IPInfo struct { 28 IP string 29 ServerId int64 30 } 31 32 type DAUManager struct { 33 isReady bool 34 35 cacheFile string 36 37 ipChan chan IPInfo 38 ipTable *kvstore.Table[[]byte] // server_DATE_serverId_ip => nil 39 40 statMap map[string]int64 // server_DATE_serverId => count 41 statLocker sync.RWMutex 42 43 cleanTicker *time.Ticker 44 } 45 46 // NewDAUManager DAU计算器 47 func NewDAUManager() *DAUManager { 48 return &DAUManager{ 49 cacheFile: Tea.Root + "/data/stat_dau.cache", 50 statMap: map[string]int64{}, 51 cleanTicker: time.NewTicker(24 * time.Hour), 52 ipChan: make(chan IPInfo, 8192), 53 } 54 } 55 56 func (this *DAUManager) Init() error { 57 // recover from cache 58 _ = this.recover() 59 60 // create table 61 store, storeErr := kvstore.DefaultStore() 62 if storeErr != nil { 63 return storeErr 64 } 65 66 db, dbErr := store.NewDB("dau") 67 if dbErr != nil { 68 return dbErr 69 } 70 71 { 72 table, err := kvstore.NewTable[[]byte]("ip", kvstore.NewNilValueEncoder()) 73 if err != nil { 74 return err 75 } 76 db.AddTable(table) 77 this.ipTable = table 78 } 79 80 { 81 table, err := kvstore.NewTable[uint64]("stats", kvstore.NewIntValueEncoder[uint64]()) 82 if err != nil { 83 return err 84 } 85 db.AddTable(table) 86 } 87 88 // clean expires items 89 goman.New(func() { 90 idles.RunTicker(this.cleanTicker, func() { 91 err := this.CleanStats() 92 if err != nil { 93 remotelogs.Error("DAU_MANAGER", "clean stats failed: "+err.Error()) 94 } 95 }) 96 }) 97 98 // dump ip to kvstore 99 goman.New(func() { 100 // cache latest IPs to reduce kv queries 101 var cachedIPs []IPInfo 102 var maxIPs = runtime.NumCPU() * 8 103 if maxIPs <= 0 { 104 maxIPs = 8 105 } else if maxIPs > 64 { 106 maxIPs = 64 107 } 108 109 var day = fasttime.Now().Ymd() 110 111 Loop: 112 for ipInfo := range this.ipChan { 113 // check day 114 if fasttime.Now().Ymd() != day { 115 day = fasttime.Now().Ymd() 116 cachedIPs = []IPInfo{} 117 } 118 119 // lookup cache 120 for _, cachedIP := range cachedIPs { 121 if cachedIP.IP == ipInfo.IP && cachedIP.ServerId == ipInfo.ServerId { 122 continue Loop 123 } 124 } 125 126 // add to cache 127 cachedIPs = append(cachedIPs, ipInfo) 128 if len(cachedIPs) > maxIPs { 129 cachedIPs = cachedIPs[1:] 130 } 131 132 _ = this.processIP(ipInfo.ServerId, ipInfo.IP) 133 } 134 }) 135 136 // dump to cache when close 137 events.OnClose(func() { 138 _ = this.Close() 139 }) 140 141 this.isReady = true 142 143 return nil 144 } 145 146 func (this *DAUManager) AddIP(serverId int64, ip string) { 147 select { 148 case this.ipChan <- IPInfo{ 149 IP: ip, 150 ServerId: serverId, 151 }: 152 default: 153 } 154 } 155 156 func (this *DAUManager) processIP(serverId int64, ip string) error { 157 if !this.isReady { 158 return nil 159 } 160 161 // day 162 var date = fasttime.Now().Ymd() 163 164 { 165 var key = "server_" + date + "_" + types.String(serverId) + "_" + ip 166 found, err := this.ipTable.Exist(key) 167 if err != nil || found { 168 return err 169 } 170 171 err = this.ipTable.Set(key, nil) 172 if err != nil { 173 return err 174 } 175 } 176 177 { 178 var key = "server_" + date + "_" + types.String(serverId) 179 this.statLocker.Lock() 180 this.statMap[key] = this.statMap[key] + 1 181 this.statLocker.Unlock() 182 } 183 184 return nil 185 } 186 187 func (this *DAUManager) ReadStatMap() map[string]int64 { 188 this.statLocker.Lock() 189 var statMap = this.statMap 190 this.statMap = map[string]int64{} 191 this.statLocker.Unlock() 192 return statMap 193 } 194 195 func (this *DAUManager) Flush() error { 196 return this.ipTable.DB().Store().Flush() 197 } 198 199 func (this *DAUManager) TestInspect(t *testing.T) { 200 err := this.ipTable.DB().Inspect(func(key []byte, value []byte) { 201 t.Log(string(key), "=>", string(value)) 202 }) 203 if err != nil { 204 t.Fatal(err) 205 } 206 } 207 208 func (this *DAUManager) Close() error { 209 this.cleanTicker.Stop() 210 211 this.statLocker.Lock() 212 var statMap = this.statMap 213 this.statMap = map[string]int64{} 214 this.statLocker.Unlock() 215 216 if len(statMap) == 0 { 217 return nil 218 } 219 220 statJSON, err := json.Marshal(statMap) 221 if err != nil { 222 return err 223 } 224 225 return os.WriteFile(this.cacheFile, statJSON, 0666) 226 } 227 228 func (this *DAUManager) CleanStats() error { 229 if !this.isReady { 230 return nil 231 } 232 233 var tr = trackers.Begin("STAT:DAU_CLEAN_STATS") 234 defer tr.End() 235 236 // day 237 { 238 var date = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -2)) 239 err := this.ipTable.DeleteRange("server_", "server_"+date) 240 if err != nil { 241 return err 242 } 243 } 244 245 return nil 246 } 247 248 func (this *DAUManager) Truncate() error { 249 return this.ipTable.Truncate() 250 } 251 252 func (this *DAUManager) recover() error { 253 data, err := os.ReadFile(this.cacheFile) 254 if err != nil || len(data) == 0 { 255 return err 256 } 257 258 _ = os.Remove(this.cacheFile) 259 260 var statMap = map[string]int64{} 261 err = json.Unmarshal(data, &statMap) 262 if err != nil { 263 return err 264 } 265 266 var today = timeutil.Format("Ymd") 267 for key := range statMap { 268 var pieces = strings.Split(key, "_") 269 if pieces[1] != today { 270 delete(statMap, key) 271 } 272 } 273 this.statMap = statMap 274 return nil 275 }