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  }