- 抢购调度重构:参考 dygou_go 将 sniper 拆分为独立子模块(runner/run_control/retry/exchange/classify/config/summary/category/helpers);移除全局自适应降速与轮次上限,采用账号独立循环、统一账号级退避、单请求并发闸门与首发错峰 - 预热与建连优化:默认关闭预热(避免单账号超时连坐整机开抢),开启时转为后台非阻塞协程;WebSocket 握手超时从 10s 降至 1500ms - 双时间日志:logger 增加 SetServerOffset,校准后日志前缀自动渲染「本地时间(H虎牙估算时间)」,实现跨机器绝对毫秒对齐 - 默认配置调优:版本提升至 v0.5.0,默认线程调为 2(缓解多实例并发限流),前端自动平滑迁移 - 验证方式:go test ./... -count=1 全通,npm run build 构建正常,单测覆盖双时间与预热配置
237 lines
6.3 KiB
Go
237 lines
6.3 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func initForTest(t *testing.T) (*Store, string, func()) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
store := NewStore(200)
|
|
closeLog, err := Init(store, dir, "测试实例", "test", dir)
|
|
if err != nil {
|
|
t.Fatalf("Init() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
SetMinLevel(LevelInfo)
|
|
SetEmitter(nil)
|
|
})
|
|
return store, FilePath(), closeLog
|
|
}
|
|
|
|
// TestInitWritesHeaderAndStructuredLines 验证日志文件带实例表头,
|
|
// 且每一行包含分类、级别与账号标签。
|
|
func TestInitWritesHeaderAndStructuredLines(t *testing.T) {
|
|
store, path, closeLog := initForTest(t)
|
|
if path == "" {
|
|
t.Fatal("日志文件路径为空")
|
|
}
|
|
|
|
LogEvent("配置", LevelInfo, "活动 SID=%d", 2203)
|
|
LogAccountEvent("监控", LevelSuccess, "88231", "兑换成功 pid=%d", 12889)
|
|
closeLog()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("读取日志文件失败: %v", err)
|
|
}
|
|
text := string(data)
|
|
|
|
for _, want := range []string{
|
|
"实例: 测试实例 (test)",
|
|
"数据目录:",
|
|
"[配置][info] 活动 SID=2203",
|
|
"[监控][success][账号:88231] 兑换成功 pid=12889",
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("日志文件缺少 %q:\n%s", want, text)
|
|
}
|
|
}
|
|
if store.Count() < 3 {
|
|
t.Fatalf("内存日志条数 = %d, want >= 3", store.Count())
|
|
}
|
|
}
|
|
|
|
// TestUniqueLogFileAvoidsSameSecondCollision 同一秒内两次创建也不能拿到同一个文件:
|
|
// 多开或快速重启时,两个进程写同一个文件会让日志互相污染。
|
|
func TestUniqueLogFileAvoidsSameSecondCollision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
first, err := newFileWriter(dir, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer first.Close()
|
|
second, err := newFileWriter(dir, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer second.Close()
|
|
|
|
if first.Path() == second.Path() {
|
|
t.Fatalf("两个进程拿到了同一个日志文件: %s", first.Path())
|
|
}
|
|
if filepath.Dir(first.Path()) != dir {
|
|
t.Fatalf("日志目录 = %q, want %q", filepath.Dir(first.Path()), dir)
|
|
}
|
|
}
|
|
|
|
// TestFileWriterRotatesAndDropsOldest 验证按大小滚动并丢弃最旧的备份。
|
|
func TestFileWriterRotatesAndDropsOldest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writer, err := newFileWriter(dir, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer writer.Close()
|
|
writer.maxBytes = 200
|
|
writer.backups = 2
|
|
|
|
for i := 0; i < 60; i++ {
|
|
if _, err := writer.Write([]byte(fmt.Sprintf("第 %d 条日志,用于触发滚动\n", i))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(writer.Path() + ".1"); err != nil {
|
|
t.Fatalf("期望存在滚动文件 .1: %v", err)
|
|
}
|
|
if _, err := os.Stat(writer.Path() + ".2"); err != nil {
|
|
t.Fatalf("期望存在滚动文件 .2: %v", err)
|
|
}
|
|
if _, err := os.Stat(writer.Path() + ".3"); err == nil {
|
|
t.Fatal("backups=2 时不应出现 .3")
|
|
}
|
|
}
|
|
|
|
type blockingWriter struct {
|
|
release chan struct{}
|
|
}
|
|
|
|
func (w blockingWriter) Write(p []byte) (int, error) {
|
|
<-w.release
|
|
return len(p), nil
|
|
}
|
|
|
|
// TestAsyncWriterDropsInsteadOfBlocking 队列满时必须丢弃日志而不是阻塞调用方,
|
|
// 否则抢购主链路会被磁盘 I/O 拖住。
|
|
func TestAsyncWriterDropsInsteadOfBlocking(t *testing.T) {
|
|
release := make(chan struct{})
|
|
writer := NewAsyncWriter(blockingWriter{release: release}, 1)
|
|
|
|
start := time.Now()
|
|
for i := 0; i < 100; i++ {
|
|
if _, err := writer.Write([]byte("x\n")); err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
}
|
|
if elapsed := time.Since(start); elapsed > time.Second {
|
|
t.Fatalf("写入被阻塞了 %s", elapsed)
|
|
}
|
|
if writer.Dropped() == 0 {
|
|
t.Fatal("队列满时应记录丢弃条数")
|
|
}
|
|
|
|
close(release)
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// TestStoreSinceIsIncremental 验证界面增量拉取语义:只返回 ID 更大的日志。
|
|
func TestStoreSinceIsIncremental(t *testing.T) {
|
|
store := NewStore(3)
|
|
first := store.Append(Entry{Message: "第一条"})
|
|
if first.ID == 0 {
|
|
t.Fatal("Append 应分配自增 ID")
|
|
}
|
|
store.Append(Entry{Message: "第二条"})
|
|
|
|
if got := store.Since(0); len(got) != 2 {
|
|
t.Fatalf("Since(0) 条数 = %d, want 2", len(got))
|
|
}
|
|
if got := store.Since(first.ID); len(got) != 1 || got[0].Message != "第二条" {
|
|
t.Fatalf("Since(first) = %+v", got)
|
|
}
|
|
|
|
// 超出容量时丢弃最旧的条目。
|
|
store.Append(Entry{Message: "第三条"})
|
|
store.Append(Entry{Message: "第四条"})
|
|
if got := store.Since(0); len(got) != 3 || got[0].Message != "第二条" {
|
|
t.Fatalf("环形缓冲 = %+v", got)
|
|
}
|
|
if store.Count() != 3 {
|
|
t.Fatalf("Count() = %d, want 3", store.Count())
|
|
}
|
|
}
|
|
|
|
// TestMinLevelFiltersDebug 低等级日志不进入内存缓冲,也不落盘。
|
|
func TestMinLevelFiltersDebug(t *testing.T) {
|
|
store, _, closeLog := initForTest(t)
|
|
defer func() {
|
|
SetMinLevel(LevelInfo)
|
|
closeLog()
|
|
}()
|
|
|
|
SetMinLevel(LevelDebug)
|
|
LogEvent("网络", LevelDebug, "debug 应保留")
|
|
if MinLevel() != LevelDebug {
|
|
t.Fatalf("MinLevel() = %q", MinLevel())
|
|
}
|
|
|
|
SetMinLevel(LevelError)
|
|
LogEvent("网络", LevelDebug, "debug 应被过滤")
|
|
if MinLevel() != LevelError {
|
|
t.Fatalf("MinLevel() = %q", MinLevel())
|
|
}
|
|
|
|
for _, entry := range store.Since(0) {
|
|
if entry.Message == "debug 应被过滤" {
|
|
t.Fatal("低于最低等级的日志不应进入内存缓冲")
|
|
}
|
|
}
|
|
if got := LevelFromString("不是等级"); got != LevelInfo {
|
|
t.Fatalf("LevelFromString(非法) = %q, want info", got)
|
|
}
|
|
}
|
|
|
|
// TestDoubleTimeWithServerOffset 验证设置服务端时间偏移后,日志带双时间前缀。
|
|
func TestDoubleTimeWithServerOffset(t *testing.T) {
|
|
store, path, closeLog := initForTest(t)
|
|
defer func() {
|
|
SetServerOffset(0)
|
|
closeLog()
|
|
}()
|
|
|
|
// 模拟服务端比本地快 500ms
|
|
SetServerOffset(500 * time.Millisecond)
|
|
if ServerOffset() != 500*time.Millisecond {
|
|
t.Fatalf("ServerOffset() = %v, want 500ms", ServerOffset())
|
|
}
|
|
|
|
LogEvent("监控", LevelInfo, "双时间测试消息")
|
|
closeLog()
|
|
|
|
entries := store.Since(0)
|
|
if len(entries) == 0 {
|
|
t.Fatal("未写入日志条目")
|
|
}
|
|
last := entries[len(entries)-1]
|
|
if !strings.Contains(last.Time, "(H") {
|
|
t.Fatalf("条目 Time 未包含虎牙估算时间前缀: %s", last.Time)
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("读取日志文件失败: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), "(H") {
|
|
t.Fatalf("文件未包含虎牙估算时间前缀:\n%s", string(data))
|
|
}
|
|
}
|
|
|