diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/db/clickhouse.go b/db/clickhouse.go new file mode 100644 index 0000000..1316925 --- /dev/null +++ b/db/clickhouse.go @@ -0,0 +1,66 @@ +package db + +import ( + "database/sql" + "fmt" + "github.com/ClickHouse/clickhouse-go/v2" + "time" +) + +type ClickHouseRepo struct { + host string + port string + user string + password string + database string + connection *sql.DB +} + +func NewClickHouseRepo(host, port, user, password, database string) *ClickHouseRepo { + return &ClickHouseRepo{ + host: host, + port: port, + user: user, + password: password, + database: database, + } +} + +func (c *ClickHouseRepo) Open() error { + c.connection = clickhouse.OpenDB(&clickhouse.Options{ + Addr: []string{fmt.Sprintf("%v:%v", c.host, c.port)}, + Auth: clickhouse.Auth{ + Database: c.database, + Username: c.user, + Password: c.password, + }, + Settings: clickhouse.Settings{ + "max_execution_time": 60, + }, + DialTimeout: 5 * time.Second, + Compression: &clickhouse.Compression{ + Method: clickhouse.CompressionLZ4, + Level: 5, + }, + Protocol: clickhouse.Native, + // Debug: true, + }) + if c.connection == nil { + return fmt.Errorf("connect clickhouse fail") + } + c.connection.SetMaxIdleConns(5) + c.connection.SetMaxOpenConns(10) + c.connection.SetConnMaxLifetime(0) + return c.connection.Ping() +} + +func (c *ClickHouseRepo) Close() { + _ = c.connection.Close() +} + +func (c *ClickHouseRepo) Select(query string, args ...interface{}) (*sql.Rows, error) { + return c.connection.Query(query, args...) +} +func (c *ClickHouseRepo) Exec(query string, args ...interface{}) (sql.Result, error) { + return c.connection.Exec(query, args...) +} diff --git a/db/clickhouse_test.go b/db/clickhouse_test.go new file mode 100644 index 0000000..0be2480 --- /dev/null +++ b/db/clickhouse_test.go @@ -0,0 +1,97 @@ +package db + +import ( + "fmt" + "strings" + "testing" +) + +func testCreateClickHouse(_ *testing.T) *ClickHouseRepo { + host := "192.168.2.224" + port := "9000" + // port = 8123 + username := "default" + password := "123456" + database := "samba" + return NewClickHouseRepo(host, port, username, password, database) +} + +func testSelect(t *testing.T) { + ch := testCreateClickHouse(t) + err := ch.Open() + if err != nil { + t.Fatal(err) + } + defer ch.Close() + rows, err := ch.Select("select user_id, coins from coins_flow limit 1") + if err != nil { + t.Fatal(err) + } + defer func() { _ = rows.Close() }() + cols, err := rows.Columns() + if err != nil { + t.Fatal(err) + } + fmt.Println(strings.Join(cols, ",")) + // 一行数据,使用any,避开数据类型问题 + var vRows = make([]any, len(cols)) + // 存实际的值,byte数组,长度以列的数量为准 + var values = make([][]byte, len(cols)) + for i := 0; i < len(cols); i++ { + vRows[i] = &values[i] + } + for rows.Next() { + err = rows.Scan(vRows...) + if err != nil { + fmt.Println(err.Error()) + break + } + var vString []string + for _, v := range values { + vString = append(vString, string(v)) + } + fmt.Println(vString, ",") + } +} + +func testInsert(t *testing.T) { + ch := testCreateClickHouse(t) + err := ch.Open() + if err != nil { + t.Fatal(err) + } + defer ch.Close() + _, err = ch.Exec("INSERT INTO coins_flow(user_id, coins) VALUES (11,11)", 11, 11) + if err != nil { + t.Fatal(err) + } + // last, err := result.LastInsertId() + // if err != nil { + // t.Fatal(err) + // } + // fmt.Println(last) +} + +func testDelete(t *testing.T) { + ch := testCreateClickHouse(t) + err := ch.Open() + if err != nil { + t.Fatal(err) + } + defer ch.Close() + _, err = ch.Exec("DELETE FROM coins_flow WHERE user_id = 11 AND coins = 11") + if err != nil { + t.Fatal(err) + } + // last, err := result.LastInsertId() + // if err != nil { + // t.Fatal(err) + // } + // fmt.Println(last) +} + +func TestClickHouse(t *testing.T) { + testSelect(t) + // testInsert(t) + testDelete(t) +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..c816f82 --- /dev/null +++ b/db/db.go @@ -0,0 +1,36 @@ +package db + +import ( + "fmt" + "github.com/go-redis/redis/v8" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func InitMysql(username, password, address, port, dbName string) (*gorm.DB, error) { + dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", username, password, address, port, dbName) + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: &dbLogger{}}) + return db, err +} + +func InitRedis(password, address, port string, dbName int) (*redis.Client, error) { + rdb := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("%v:%v", address, port), Password: password, DB: dbName}) + if rdb == nil { + return nil, fmt.Errorf("init redis fail") + } + return rdb, nil +} + +func InitClickHouse(host, port, user, password, database string) (*ClickHouseRepo, error) { + ch := NewClickHouseRepo(host, port, user, password, database) + return ch, ch.Open() +} + +/* +sudo docker run -d \ + --name my-redis \ + -p 6379:6379 \ + -e REDIS_PASSWORD=fox379@@zyxi \ + redis:latest \ + --requirepass fox379@@zyxi +*/ diff --git a/db/dblog.go b/db/dblog.go new file mode 100644 index 0000000..809a3a2 --- /dev/null +++ b/db/dblog.go @@ -0,0 +1,46 @@ +package db + +import ( + "context" + "fmt" + "github.com/fox/fox/log" + "gorm.io/gorm/logger" + "strings" + "time" +) + +type dbLogger struct{} + +func (l *dbLogger) LogMode(_ logger.LogLevel) logger.Interface { + // 可以在这里根据 logLevel 做出不同的处理 + return l +} + +func (l *dbLogger) Info(_ context.Context, msg string, args ...interface{}) { + log.InfoF(msg, args...) +} + +func (l *dbLogger) Warn(_ context.Context, msg string, args ...interface{}) { + log.WarnF(msg, args...) +} + +func (l *dbLogger) Error(_ context.Context, msg string, args ...interface{}) { + s := fmt.Sprintf(msg, args...) + if !strings.Contains(s, "record not found") { + // 只有当错误不是“record not found”时才记录 + log.ErrorF(msg, args...) + } + +} + +func (l *dbLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + if err != nil { + // SQL 执行错误 + l.Error(ctx, "Error occurred while executing SQL: %v", err) + return + } + + sql, rows := fc() + elapsed := time.Since(begin) + log.DebugF("SQL: %s, RowsAffected: %d, Elapsed: %s\n", sql, rows, elapsed) +} diff --git a/etcd/etcd.go b/etcd/etcd.go new file mode 100644 index 0000000..e332faa --- /dev/null +++ b/etcd/etcd.go @@ -0,0 +1,65 @@ +package etcd + +import ( + "encoding/json" + "fmt" + "github.com/fox/fox/log" + "sync" +) + +type resultT[T any] struct { + Value T + Err error +} + +type Registry[T INode] struct { + *etcdRegistryImpl + nodes sync.Map +} + +func NewRegistry[T INode](endpoints []string, rootKey, username, password string) (*Registry[T], error) { + var err error + e := &Registry[T]{} + e.etcdRegistryImpl, err = newServiceRegistryImpl(endpoints, rootKey, username, password, e.saveNode) + return e, err +} + +func (e *Registry[T]) Register(node INode) error { + bs, err := json.Marshal(node) + if err != nil { + return err + } + return e.etcdRegistryImpl.Register(node.EtcdKey(), string(bs)) +} + +// 获取当前服务 +func (sr *Registry[T]) saveNode(jsonBytes []byte) { + var tmp = resultT[T]{Err: nil} + if err := json.Unmarshal(jsonBytes, &tmp.Value); err != nil { + log.ErrorF(err.Error()) + } + sr.nodes.Store(tmp.Value.MapKey(), tmp.Value) +} + +// 获取当前根节点下所有节点信息 +func (sr *Registry[T]) GetNodes() []T { + var nodes []T + sr.nodes.Range(func(k, v interface{}) bool { + nodes = append(nodes, v.(T)) + return true + }) + return nodes +} + +// 获取当前根节点下所有节点信息 +func (sr *Registry[T]) FindNode(key string) (T, error) { + var tmp = resultT[T]{Err: nil} + v, ok := sr.nodes.Load(key) + if !ok { + return tmp.Value, fmt.Errorf("%v not exist", key) + } + if tmp.Value, ok = v.(T); ok { + return tmp.Value, nil + } + return tmp.Value, fmt.Errorf("%v 类型转换失败", key) +} diff --git a/etcd/etcdImpl.go b/etcd/etcdImpl.go new file mode 100644 index 0000000..b919ed7 --- /dev/null +++ b/etcd/etcdImpl.go @@ -0,0 +1,167 @@ +package etcd + +import ( + "context" + "fmt" + "github.com/fox/fox/ksync" + "github.com/fox/fox/log" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +const ( + DefaultDialTimeout = 3 * time.Second // 默认拨号超时时间 + DefaultLeaseTTL = int64(10) // 默认租约TTL为60秒 + DefaultKeepAliveInterval = 5 * time.Second // 默认续租间隔为30秒 + KeepAliveFailCount = 100 +) + +type etcdRegistryImpl struct { + cli *clientv3.Client + leaseID clientv3.LeaseID + nodeKey string + cancelFunc context.CancelFunc + rootKey string + saveNodeFunc func(jsonBytes []byte) +} + +// 创建服务注册中心 +func newServiceRegistryImpl(endpoints []string, rootKey, username, password string, saveNode func([]byte)) (*etcdRegistryImpl, error) { + cli, err := clientv3.New(clientv3.Config{ + Endpoints: endpoints, + DialTimeout: DefaultDialTimeout, + Username: username, + Password: password, + }) + if err != nil { + return nil, err + } + + return &etcdRegistryImpl{ + cli: cli, + rootKey: rootKey, + saveNodeFunc: saveNode, + }, nil +} + +// 注册服务 RegisterService +func (sr *etcdRegistryImpl) Register(key, value string) error { + // 生成唯一服务key + // /services/serviceType/serviceName + sr.nodeKey = key + log.DebugF("register %s to etcd", key) + + // 创建租约 + ctx, cancel := context.WithCancel(context.Background()) + sr.cancelFunc = cancel + + // 申请租约 + resp, err := sr.cli.Grant(ctx, DefaultLeaseTTL) + if err != nil { + return err + } + sr.leaseID = resp.ID + + // 序列化服务信息 + // data, err := json.Marshal(node) + // if err != nil { + // return err + // } + + // 写入ETCD + _, err = sr.cli.Put(ctx, key, value, clientv3.WithLease(sr.leaseID)) + if err != nil { + return err + } + + // 启动自动续租 + go sr.keepAlive(ctx) + + if err = sr.discoverServices(); err == nil { + // ss := sr.GetService() + // for _, s := range ss { + // log.Debug(fmt.Sprintf("Discovered services: %+v\n", s)) + // } + + } + return nil +} + +// 自动续租逻辑 +func (sr *etcdRegistryImpl) keepAlive(ctx context.Context) { + retryCount := 0 + ticker := time.NewTicker(DefaultKeepAliveInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + _, err := sr.cli.KeepAliveOnce(ctx, sr.leaseID) + if err != nil { + if retryCount > KeepAliveFailCount { + log.DebugF("KeepAlive failed after %d retries: %v", KeepAliveFailCount, err) + sr.UnregisterService() + return + } + retryCount++ + // log.DebugF("KeepAlive error (retry %d/%d): %v", retryCount, KeepAliveFailCount, err) + } else { + retryCount = 0 + } + case <-ctx.Done(): + return + } + } +} + +// 反注册服务 +func (sr *etcdRegistryImpl) UnregisterService() { + if sr.cancelFunc != nil { + sr.cancelFunc() + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if _, err := sr.cli.Delete(ctx, sr.nodeKey); err != nil { + log.ErrorF("unregister:%v failed:%v from etcd", sr.nodeKey, err) + } else { + log.DebugF("unregister:%v from etcd", sr.nodeKey) + } +} + +// 服务发现 +func (sr *etcdRegistryImpl) discoverServices() error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + srv := fmt.Sprintf("/%s/", sr.rootKey) + resp, err := sr.cli.Get(ctx, srv, clientv3.WithPrefix()) + if err != nil { + return err + } + // log.Debug(fmt.Sprintf("discoverServices srv:%s", srv)) + + for _, kv := range resp.Kvs { + sr.saveNodeFunc(kv.Value) + } + + return nil +} + +// 监控服务变化 +func (sr *etcdRegistryImpl) WatchServices() { + watchKey := fmt.Sprintf("/%s/", sr.rootKey) + ksync.GoSafe(func() { + rch := sr.cli.Watch(context.Background(), watchKey, clientv3.WithPrefix()) + for resp := range rch { + for range resp.Events { + // 当有变化时获取最新服务列表 + if err := sr.discoverServices(); err != nil { + log.Error(err.Error()) + } + } + } + }, nil) +} diff --git a/etcd/etcd_test.go b/etcd/etcd_test.go new file mode 100644 index 0000000..045d8ec --- /dev/null +++ b/etcd/etcd_test.go @@ -0,0 +1,91 @@ +package etcd + +import ( + "github.com/fox/fox/log" + "testing" + "time" +) + +const ( + etcdAddress1 = "192.168.232.128:2379" + etcdAddress2 = "114.132.124.145:2379" +) + +/* + sudo docker run -d \ + --name my-etcd \ + -p 2379:2379 \ + -p 2380:2380 \ + quay.io/coreos/etcd:v3.6.0 \ + etcd \ + --name etcd-single \ + --data-dir /etcd-data \ + --initial-advertise-peer-urls http://0.0.0.0:2380 \ + --listen-peer-urls http://0.0.0.0:2380 \ + --advertise-client-urls http://0.0.0.0:2379 \ + --listen-client-urls http://0.0.0.0:2379 \ + --initial-cluster etcd-single=http://0.0.0.0:2380 + +sudo docker run -d --name etcdkeeper -p 8080:8080 evildecay/etcdkeeper +*/ +func TestService(t *testing.T) { + _ = etcdAddress1 + _ = etcdAddress2 + log.Open("test.log", log.DebugL) + // 创建注册中心 + registry, err := NewRegistry[ServiceNode]([]string{etcdAddress2}, rootKeyServices, "", "") + if err != nil { + log.Fatal(err.Error()) + } + + // 注册示例服务 + service := &ServiceNode{ + Name: "instance-1", + Type: "user-service", + Address: "localhost", + Port: 8080, + } + + if err := registry.Register(service); err != nil { + log.Fatal(err.Error()) + } + + // 监控服务变化 + registry.WatchServices() + + time.Sleep(10 * time.Second) + registry.UnregisterService() +} + +func TestTopicRegistry(t *testing.T) { + _ = etcdAddress1 + _ = etcdAddress2 + log.Open("test.log", log.DebugL) + // 创建注册中心 + registry, err := NewRegistry[TopicNode]([]string{etcdAddress2}, rootKeyTopic, "", "") + if err != nil { + log.Fatal(err.Error()) + } + + // 注册示例服务 + node := &TopicNode{ + Name: "instance-1", + Creator: "instance-1", + } + + if err := registry.Register(node); err != nil { + log.Fatal(err.Error()) + } + + // 监控服务变化 + registry.WatchServices() + for _, n := range registry.GetNodes() { + log.DebugF("发现topic:%v, 创建者:%v", n.Name, n.Creator) + if v, err := registry.FindNode(n.MapKey()); err == nil { + log.DebugF("topic:%v exist", v.Name) + } + } + + time.Sleep(60 * time.Second) + registry.UnregisterService() +} diff --git a/etcd/inode.go b/etcd/inode.go new file mode 100644 index 0000000..fde2260 --- /dev/null +++ b/etcd/inode.go @@ -0,0 +1,53 @@ +package etcd + +import "fmt" + +const ( + rootKeyServices = "services" + rootKeyTopic = "topic" +) + +type INode interface { + // 注册到etcd的key + EtcdKey() string + EtcdRootKey() string + MapKey() string +} + +type ServiceNode struct { + Name string `json:"name"` // 服务名 多个同类服务依赖name区分:1,2,3等等 + Type string `json:"type"` // 服务类型:lobby, game, gate等等 + Address string `json:"address"` // 地址 + Port int `json:"port"` // 端口 + Version string `json:"version"` // 版本号 + ServiceType ServiceType `json:"service_type"` // 服务类型 +} + +func (s ServiceNode) EtcdKey() string { + return fmt.Sprintf("/%s/%s/%s", rootKeyServices, s.Type, s.Name) +} + +func (s ServiceNode) EtcdRootKey() string { + return rootKeyServices +} + +func (s ServiceNode) MapKey() string { + return fmt.Sprintf("%s-%s", s.Type, s.Name) +} + +type TopicNode struct { + Name string `json:"name"` // topic名 + Creator string `json:"creator"` // topic创建者 +} + +func (s TopicNode) EtcdKey() string { + return fmt.Sprintf("/%s/%s/%s", rootKeyTopic, s.Creator, s.Name) +} + +func (s TopicNode) EtcdRootKey() string { + return rootKeyTopic +} + +func (s TopicNode) MapKey() string { + return fmt.Sprintf("%s", s.Name) +} diff --git a/etcd/serviceType.go b/etcd/serviceType.go new file mode 100644 index 0000000..ae1ba5e --- /dev/null +++ b/etcd/serviceType.go @@ -0,0 +1,19 @@ +package etcd + +type ServiceType int + +const ( + Unique ServiceType = 1 // 唯一 + Multiple ServiceType = 2 // 多个 +) + +func (s ServiceType) String() string { + switch s { + case Unique: + return "unique" + case Multiple: + return "multiple" + default: + return "unknown" + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c3e17da --- /dev/null +++ b/go.mod @@ -0,0 +1,67 @@ +module github.com/fox/fox + +go 1.23.0 + +toolchain go1.23.7 + +require ( + github.com/ClickHouse/clickhouse-go/v2 v2.34.0 + github.com/go-redis/redis/v8 v8.11.5 + github.com/golang-module/carbon/v2 v2.6.5 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/natefinch/lumberjack v2.0.0+incompatible + github.com/nats-io/nats.go v1.42.0 + github.com/nsqio/go-nsq v1.1.0 + github.com/orcaman/concurrent-map/v2 v2.0.1 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/stretchr/testify v1.10.0 + github.com/wanghuiyt/ding v0.0.2 + go.etcd.io/etcd/client/v3 v3.5.19 + go.uber.org/zap v1.27.0 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.26.1 +) + +require ( + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/ClickHouse/ch-go v0.65.1 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dromara/carbon/v2 v2.6.5 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/paulmach/orb v0.11.1 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.19 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.19 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.62.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6945728 --- /dev/null +++ b/go.sum @@ -0,0 +1,198 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU= +github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4= +github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co= +github.com/ClickHouse/clickhouse-go/v2 v2.34.0/go.mod h1:yioSINoRLVZkLyDzdMXPLRIqhDvel8iLBlwh6Iefso8= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dromara/carbon/v2 v2.6.5 h1:OC1k8zGBpSnRoPjezlWeajx+3nCMq7xhZqAS4WWrKmE= +github.com/dromara/carbon/v2 v2.6.5/go.mod h1:7GXqCUplwN1s1b4whGk2zX4+g4CMCoDIZzmjlyt0vLY= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-module/carbon/v2 v2.6.5 h1:C3YpydJZmo77AyMsgQ1QpVDQeLJ0itOa5sQB4Y/jk4I= +github.com/golang-module/carbon/v2 v2.6.5/go.mod h1:JvSYEoe3+OcMnSQyRvUNQFpg0T/4xCq7moHu1S8ESXQ= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM= +github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= +github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/wanghuiyt/ding v0.0.2 h1:6ZISlgCSy6MVeaFR8kAdniALMRqd56GyO9LlmYdTw/s= +github.com/wanghuiyt/ding v0.0.2/go.mod h1:T1vPz74YMmGCBVKZzVsen/YAYRZ2bvBYXldUyD7Y4vc= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.5.19 h1:w3L6sQZGsWPuBxRQ4m6pPP3bVUtV8rjW033EGwlr0jw= +go.etcd.io/etcd/api/v3 v3.5.19/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U= +go.etcd.io/etcd/client/pkg/v3 v3.5.19 h1:9VsyGhg0WQGjDWWlDI4VuaS9PZJGNbPkaHEIuLwtixk= +go.etcd.io/etcd/client/pkg/v3 v3.5.19/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0= +go.etcd.io/etcd/client/v3 v3.5.19 h1:+4byIz6ti3QC28W0zB0cEZWwhpVHXdrKovyycJh1KNo= +go.etcd.io/etcd/client/v3 v3.5.19/go.mod h1:FNzyinmMIl0oVsty1zA3hFeUrxXI/JpEnz4sG+POzjU= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +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-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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= +gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/ksync/ksync.go b/ksync/ksync.go new file mode 100644 index 0000000..2b8ab74 --- /dev/null +++ b/ksync/ksync.go @@ -0,0 +1,76 @@ +package ksync + +import ( + "fmt" + "github.com/fox/fox/log" + "github.com/wanghuiyt/ding" + "runtime/debug" + "strings" +) + +// 辅助函数列表 +var ( + DingAccessToken = "" // 钉钉token + DingSecret = "" // 钉钉加签 + lastAttributeCode = "" // 最近一条error信息 +) + +func getAttributeCode(stackErr string) string { + lines := strings.Split(stackErr, "\n") + // 检查是否有足够的行数,并提取第9行(索引为8,因为索引从0开始) + if len(lines) >= 9 { + goIndex := strings.LastIndex(lines[8], ".go") + if goIndex != -1 { + filteredStr := lines[8][:goIndex] + filteredStr = strings.TrimLeft(filteredStr, "\t ") + return filteredStr + } + } + return "" +} + +// 发送消息给钉钉 +func sendMessage(msg interface{}, stackErr string, dingToken, dingSecret string) { + if dingToken != "" { + d := ding.Webhook{ + AccessToken: dingToken, // 上面获取的 access_token + Secret: dingSecret, // 上面获取的加签的值 + } + _ = d.SendMessageText(fmt.Sprintf("Recover panic:%v\n%v", msg, stackErr), "13145922265", "17353003985") + } +} + +func Recover(recoverFunc func()) { + if msg := recover(); msg != nil { + stackErr := string(debug.Stack()) + attributeCode := getAttributeCode(stackErr) + if lastAttributeCode != attributeCode { + lastAttributeCode = attributeCode + log.ErrorF("Recover panic:%v", msg) + log.Error(stackErr) + sendMessage(msg, stackErr, DingAccessToken, DingSecret) + } + if recoverFunc != nil { + recoverFunc() + } + } + +} + +// RunSafe runs the given fn, recovers if fn panics. +func RunSafe(fn func(), recoverFunc func()) { + defer Recover(recoverFunc) + fn() +} + +// GoSafe runs the given fn using another goroutine, recovers if fn panics. +func GoSafe(fn func(), recoverFunc func()) { + go RunSafe(fn, recoverFunc) +} + +// WrapSafe return a RunSafe wrap func +func WrapSafe(fn func(), recoverFunc func()) func() { + return func() { + RunSafe(fn, recoverFunc) + } +} diff --git a/ksync/ksync_test.go b/ksync/ksync_test.go new file mode 100644 index 0000000..aa82fe0 --- /dev/null +++ b/ksync/ksync_test.go @@ -0,0 +1,34 @@ +package ksync + +import ( + "fmt" + "github.com/fox/fox/log" + "github.com/wanghuiyt/ding" + "testing" + "time" +) + +func TestEtcd(t *testing.T) { + log.Open("test.log", log.DebugL) + WrapSafe(func() { fmt.Println("hello world") }, nil) +} + +func TestSendDingTalkMessage(t *testing.T) { + d := ding.Webhook{ + AccessToken: "9802639f5dea7dd4aaff98a8f264b98c224f90ceeed3c26b438534e3a79222b1", // 上面获取的 access_token + Secret: "SECffd7d2afe9d0590fd04e36a0efa44d73553fb8f702eb1690e579996aec6c1386", // 上面获取的加签的值 + } + _ = d.SendMessageText("这是普通的群消息", "13145922265", "17353003985") +} + +func TestRecover(t *testing.T) { + log.Open("test.log", log.DebugL) + cb := func() { + i := new(int) + *i = 1 + i = nil + *i = 2 + } + GoSafe(cb, cb) + time.Sleep(time.Minute) +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..245422e --- /dev/null +++ b/log/log.go @@ -0,0 +1,156 @@ +package log + +import ( + "fmt" + "github.com/natefinch/lumberjack" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "runtime" + "time" +) + +var ( + logger *zap.Logger + file *os.File +) + +const ( + DebugL = zapcore.DebugLevel + InfoL = zapcore.InfoLevel + WarnL = zapcore.WarnLevel + ErrorL = zapcore.ErrorLevel +) + +func Open(filepath string, level zapcore.Level) { + if level < DebugL || level > ErrorL { + level = DebugL + } + // 自定义时间编码器,不显示时区 + customTimeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05.000")) // 使用不含时区的格式 + } + // 配置lumberjack + file := &lumberjack.Logger{ + Filename: filepath, // 日志文件的位置 + MaxSize: 100, // 每个日志文件保存的最大尺寸 单位:MB + MaxBackups: 3, // 日志文件最多保存多少个备份 + MaxAge: 28, // 文件最多保存多少天 + Compress: true, // 是否压缩/归档旧文件 + } + + // var err error + // // 打开日志文件 + // file, err = os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + // if err != nil { + // zap.L().Fatal("无法打开日志文件", zap.Error(err)) + // } + + // 设置编码器配置 + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: customTimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } + + // 创建控制台输出 + consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) + consoleWriter := zapcore.AddSync(os.Stdout) + consoleCore := zapcore.NewCore(consoleEncoder, consoleWriter, zap.NewAtomicLevelAt(zap.DebugLevel)) + + // 创建文件输出 + fileEncoder := zapcore.NewJSONEncoder(encoderConfig) + fileWriter := zapcore.AddSync(file) + fileCore := zapcore.NewCore(fileEncoder, fileWriter, zap.NewAtomicLevelAt(level)) + + // 将两个 Core 组合成一个 MultiWriteSyncer + core := zapcore.NewTee(consoleCore, fileCore) + + // 创建logger对象 + logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) + + // Info = logger.Info + // Debug = logger.Debug + // Warn = logger.Warn + // Error = logger.Error + // Fatal = logger.Fatal +} + +func Close() { + if logger != nil { + _ = logger.Sync() + logger = nil + } + if file != nil { + _ = file.Close() + file = nil + } +} + +func StackTrace() string { + var pcs [32]uintptr + n := runtime.Callers(3, pcs[:]) // 跳过前3个栈帧,即printStackTrace, caller, 和runtime.Callers本身 + frames := runtime.CallersFrames(pcs[:n]) + s := "" + for { + frame, more := frames.Next() + s += fmt.Sprintf("%+v\n", frame) + if !more { + break + } + } + return s +} + +func InfoF(format string, args ...any) { + logger.Info(fmt.Sprintf(format, args...)) +} + +func Info(msg string, fields ...zap.Field) { + logger.Info(msg, fields...) +} + +func Debug(msg string, fields ...zap.Field) { + logger.Debug(msg, fields...) +} + +func DebugF(format string, args ...any) { + logger.Debug(fmt.Sprintf(format, args...)) +} + +func Warn(msg string, fields ...zap.Field) { + logger.Warn(msg, fields...) +} + +func WarnF(format string, args ...any) { + logger.Info(fmt.Sprintf(format, args...)) +} + +func Error(msg string, fields ...zap.Field) { + logger.Error(msg, fields...) +} + +func ErrorF(format string, args ...any) { + logger.Error(fmt.Sprintf(format, args...)) +} + +func Fatal(msg string, fields ...zap.Field) { + logger.Fatal(msg, fields...) +} + +func FatalF(format string, args ...any) { + logger.Fatal(fmt.Sprintf(format, args...)) +} + +// 提供logger,方便其它库打印正确的日志调用点 +func GetLogger() *zap.Logger { + return logger +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..5938809 --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,44 @@ +package log + +import ( + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "testing" +) + +func testLogger(t *testing.T, level zapcore.Level) { + _ = t + Open("test.log", level) + defer Close() + + Debug("debug. ", zap.String("name", "liu")) + Info("info. ", zap.Error(fmt.Errorf("this is an error"))) + Warn("warn.", zap.Int64("int64", 111)) + Error("error.", zap.Bool("bool", true)) + Fatal("fatal. ", zap.String("name", "liu")) + DebugF("debugF: %.2f", 1.23) + InfoF("infoF: %.2f", 1.23) + WarnF("warnF: %.2f", 1.23) + ErrorF("errorF: %.2f", 1.23) + FatalF("fatalF: %.2f", 1.23) +} + +func TestLogger(t *testing.T) { + testLogger(t, DebugL) + testLogger(t, InfoL) + testLogger(t, WarnL) + testLogger(t, ErrorL) +} + +func testStackTrace(t *testing.T, level zapcore.Level) { + _ = t + Open("test.log", level) + defer Close() + stack := StackTrace() + Debug(stack) +} + +func TestStackTrace(t *testing.T) { + testStackTrace(t, DebugL) +} diff --git a/nat/nats.go b/nat/nats.go new file mode 100644 index 0000000..00eb993 --- /dev/null +++ b/nat/nats.go @@ -0,0 +1,147 @@ +package nat + +import ( + "fmt" + "github.com/fox/fox/log" + "github.com/fox/fox/safeChan" + "github.com/nats-io/nats.go" + "sync" + "time" +) + +type RpcHandler func(msg []byte) ([]byte, error) + +type Nats struct { + address []string + nc *nats.Conn + sub []*nats.Subscription + name string + mt sync.Mutex +} + +// []string{ +// "nats://server1:4222", +// "nats://server2:4222", +// "nats://server3:4222", +// } +func NewNats(name string, address ...string) *Nats { + n := &Nats{ + address: address, + nc: nil, + sub: nil, + name: name, + mt: sync.Mutex{}, + } + return n +} + +func (n *Nats) Connect() error { + opts := nats.GetDefaultOptions() + opts.Servers = n.address + opts.AllowReconnect = true + opts.MaxReconnect = 10 + opts.ReconnectWait = 5 * time.Second + opts.Name = n.name + + opts.ClosedCB = func(conn *nats.Conn) { + _ = conn + log.Info("nats 连接已关闭") + } + opts.DisconnectedErrCB = func(conn *nats.Conn, err error) { + if err != nil { + log.ErrorF("nats 连接断开, err:%v", err) + } + } + nc, err := opts.Connect() + if err != nil { + return err + } + n.nc = nc + log.InfoF("连接nats成功,当前服务器:%v", nc.ConnectedUrl()) + return nil +} + +func (n *Nats) Close() { + if n.nc != nil { + n.nc.Close() + } + n.clearAllSub() +} + +func (n *Nats) SubscribeRpc(topic string, rpcHandler RpcHandler) error { + if rpcHandler == nil { + return fmt.Errorf("rpc handler is nil") + } + rspErrF := func(m *nats.Msg) { + if err := m.Respond([]byte("error")); err != nil { + log.Error(err.Error()) + } + } + + sub, err := n.nc.Subscribe(topic, func(m *nats.Msg) { + rsp, err := rpcHandler(m.Data) + if err != nil { + log.Error(err.Error()) + rspErrF(m) + return + } + if err = m.Respond(rsp); err != nil { + log.Error(err.Error()) + } + }) + if err != nil { + return err + } + n.addSub(sub) + return nil +} + +func (n *Nats) Subscribe(topic string, msgChan *safeChan.ByteChan) error { + sub, err := n.nc.Subscribe(topic, func(m *nats.Msg) { + _ = msgChan.Write(m.Data) + }) + if err != nil { + return err + } + n.addSub(sub) + return nil +} + +func (n *Nats) Publish(topic string, msg []byte) error { + return n.nc.Publish(topic, msg) +} + +func (n *Nats) Rpc(topic string, msg []byte) ([]byte, error) { + rsp, err := n.nc.Request(topic, msg, 30*time.Second) + if err != nil { + return nil, err + } + return rsp.Data, nil +} + +// 队列订阅,队列中只会有一个消费者消费该消息 +func (n *Nats) QueueSubscribe(topic string, queue string, msgChan *safeChan.ByteChan) error { + sub, err := n.nc.QueueSubscribe(topic, queue, func(m *nats.Msg) { + _ = msgChan.Write(m.Data) + }) + if err != nil { + return err + } + n.addSub(sub) + return nil +} + +func (n *Nats) addSub(sub *nats.Subscription) { + n.mt.Lock() + defer n.mt.Unlock() + n.sub = append(n.sub, sub) +} + +func (n *Nats) clearAllSub() { + n.mt.Lock() + defer n.mt.Unlock() + for _, sub := range n.sub { + _ = sub.Unsubscribe() + } + n.sub = n.sub[:0] +} diff --git a/nat/nats_test.go b/nat/nats_test.go new file mode 100644 index 0000000..3442441 --- /dev/null +++ b/nat/nats_test.go @@ -0,0 +1,117 @@ +package nat + +import ( + "fmt" + "github.com/fox/fox/ksync" + "github.com/fox/fox/log" + "github.com/fox/fox/safeChan" + "sync/atomic" + "testing" + "time" +) + +const ( + nats1 = "nats://192.168.232.128:4222" + nats2 = "nats://114.132.124.145:4222" +) + +func initLog() { + _ = nats1 + _ = nats2 + log.Open("test.log", log.DebugL) +} + +/* +docker pull nats:latest +docker run -d --name my-nats -p 4222:4222 -p 8222:8222 nats +*/ +func TestNats(t *testing.T) { + initLog() + n := NewNats("test", nats2) + if err := n.Connect(); err != nil { + t.Log(err) + return + } + + topic := "test.topic" + for i := 0; i < 2; i++ { + msgChan := safeChan.NewSafeChan[[]byte](128) + if err := n.Subscribe(topic, msgChan); err != nil { + t.Log(err) + return + } + ksync.GoSafe(func() { + for { + select { + case msg, ok := <-msgChan.Reader(): + if !ok { + return + } + t.Log("consumer:", i, string(msg)) + } + } + }, nil) + } + count := 0 + for { + count++ + _ = n.Publish(topic, []byte(fmt.Sprintf("hello nats:%v", count))) + if count > 4 { + break + } + } + time.Sleep(3 * time.Second) + n.Close() +} + +func TestQueue(t *testing.T) { + initLog() + n := NewNats("test", "nats://192.168.232.128:4222") + if err := n.Connect(); err != nil { + t.Log(err) + return + } + + topic := "test.group" + queue := "test.queue" + + count2 := int32(0) + for i := 0; i < 3; i++ { + msgChan := safeChan.NewSafeChan[[]byte](128) + if err := n.QueueSubscribe(topic, queue, msgChan); err != nil { + t.Log(err) + return + } + ksync.GoSafe(func() { + for { + select { + case msg, ok := <-msgChan.Reader(): + if !ok { + return + } + _ = atomic.AddInt32(&count2, 1) + _ = msg + // t.Log("consumer:", i, string(msg)) + } + } + }, nil) + } + + count := int32(0) + for { + count++ + _ = n.Publish(topic, []byte(fmt.Sprintf("hello nats:%v", count))) + if count > 900000 { + break + } + } + time.Sleep(10 * time.Second) + c := atomic.LoadInt32(&count2) + if c == count { + t.Log("count==count2==", c) + } else { + t.Log("count:", count, " count2:", count2) + } + + n.Close() +} diff --git a/nsq/consumer.go b/nsq/consumer.go new file mode 100644 index 0000000..f8ca86e --- /dev/null +++ b/nsq/consumer.go @@ -0,0 +1,128 @@ +package nsq + +import ( + "fmt" + "github.com/fox/fox/log" + "github.com/nsqio/go-nsq" + "time" +) + +const ( + chanLen = 20 // 消息队列容量 +) + +type chanState int + +const ( + idle chanState = 0 + busy chanState = 1 +) + +func (ch chanState) String() string { + switch ch { + case idle: + return "idle" + case busy: + return "busy" + default: + return "unknown" + } +} + +type Consumer struct { + name string + addr string + topic string + channel string + msgChan chan *nsq.Message + consumer *nsq.Consumer + state chanState +} + +func newConsumer(nsqAddr, topic, channel string) (*Consumer, error) { + var err error + config := nsq.NewConfig() + // 心跳设置 + config.HeartbeatInterval = 30 * time.Second + // 关键配置项 + // config.DialTimeout = 10 * time.Second // TCP 连接超时(默认系统级) + // config.ReadTimeout = 60 * time.Second // 读取数据超时 + // config.WriteTimeout = 60 * time.Second // 写入数据超时 + // + // // 如果是通过 nsqlookupd 发现服务,调整 HTTP 客户端超时 + // config.LookupdPollInterval = 5 * time.Second // 轮询间隔 + // config.LookupdPollTimeout = 10 * time.Second // 单个 HTTP 请求超时 + + c := &Consumer{} + c.addr = nsqAddr + c.state = idle + c.topic = topic + c.channel = channel + c.name = fmt.Sprintf("%v-%v", topic, channel) + c.msgChan = make(chan *nsq.Message, chanLen) + c.consumer, err = nsq.NewConsumer(topic, channel, config) + if err != nil { + return nil, err + } + c.consumer.AddHandler(c) + + c.consumer.SetLogger(newNSQLogger(nsq.LogLevelError), nsq.LogLevelError) + return c, nil +} + +// 创建消费者,直连NSQd发现服务 +func NewConsumerByNsqD(nsqDAddr, topic, channel string) (*Consumer, error) { + c, err := newConsumer(nsqDAddr, topic, channel) + if err != nil { + return nil, err + } + // 连接方式选择:直连NSQd + if err = c.consumer.ConnectToNSQD(nsqDAddr); err != nil { + return nil, err + } + log.DebugF("consumer %v-%v 注册成功", topic, channel) + return c, err +} + +// 创建消费者,连接NSQLookupD发现服务 +func NewConsumer(lookupAddr, topic, channel string) (*Consumer, error) { + c, err := newConsumer(lookupAddr, topic, channel) + if err != nil { + return nil, err + } + // 连接NSQLookupD发现服务 + err = c.consumer.ConnectToNSQLookupd(c.addr) + if err != nil { + return nil, err + } + log.DebugF("consumer %v-%v 注册成功", topic, channel) + return c, err +} + +func (c *Consumer) HandleMessage(msg *nsq.Message) error { + num := len(c.msgChan) + if num > chanLen/2 && c.state == idle { + c.state = busy + log.WarnF("%v-%v 通道已从闲时转为繁忙状态。当前积压消息数:%v", c.topic, c.channel, len(c.msgChan)) + } else if c.state == busy && num < chanLen/4 { + c.state = idle + log.WarnF("%v-%v 通道已从繁忙转为闲时状态。当前积压消息数:%v", c.topic, c.channel, len(c.msgChan)) + } + c.msgChan <- msg + msg.Finish() + return nil +} + +func (c *Consumer) Name() string { + return c.name +} + +func (c *Consumer) Read() <-chan *nsq.Message { + return c.msgChan +} + +func (c *Consumer) Close() { + if c.consumer != nil { + c.consumer.Stop() + } +} diff --git a/nsq/nsq_test.go b/nsq/nsq_test.go new file mode 100644 index 0000000..f25c1e8 --- /dev/null +++ b/nsq/nsq_test.go @@ -0,0 +1,93 @@ +package nsq + +import ( + "fmt" + "github.com/fox/fox/log" + "github.com/nsqio/go-nsq" + "go.uber.org/zap" + "testing" + "time" +) + +const ( + testTopic = "test_topic" + testChannel = "test_channel" + testChannelD = "test_channelD" + testAddress = "192.168.232.128:4150" + testLookupAddress = "192.168.232.128:4161" +) + +func initLog() { + log.Open("test.log", log.DebugL) +} + +func toString(id nsq.MessageID) string { + sid := "" + for _, b := range id { + sid += string(b) + } + return sid +} + +func testProducer() *Producer { + // 初始化一个生产者 + producer, err := NewProducer(testAddress) + if err != nil { + log.Fatal(err.Error()) + return nil + } + + for n := 0; n <= 20; n++ { + err = producer.Publish(testTopic, []byte(fmt.Sprintf("hello nsq %d", n))) + if err != nil { + log.Fatal(err.Error()) + return nil + } + // time.Sleep(1 * time.Second) + } + return producer +} + +func testConsumers() { + consumer, err := NewConsumerByNsqD(testAddress, testTopic, testChannel) + if err != nil { + log.Fatal(err.Error()) + return + } + defer consumer.Close() + time.Sleep(2 * time.Second) + for { + select { + case msg := <-consumer.Read(): + log.Debug(consumer.Name(), zap.String("id", toString(msg.ID)), zap.String("body", string(msg.Body))) + } + } +} + +func testConsumerByLookupD() { + consumer, err := NewConsumer(testLookupAddress, testTopic, testChannelD) + if err != nil { + log.Fatal(err.Error()) + return + } + defer consumer.Close() + time.Sleep(2 * time.Second) + for { + select { + case msg := <-consumer.Read(): + log.Debug(consumer.name, zap.String("id", toString(msg.ID)), zap.String("body", string(msg.Body))) + } + } +} + +func TestNsq(t *testing.T) { + initLog() + // go testConsumers() + // go testConsumerByLookupD() + producer := testProducer() + if producer != nil { + defer producer.Close() + } + time.Sleep(5 * time.Second) + log.Debug("shutdown") +} diff --git a/nsq/nsqlog.go b/nsq/nsqlog.go new file mode 100644 index 0000000..fe09725 --- /dev/null +++ b/nsq/nsqlog.go @@ -0,0 +1,31 @@ +package nsq + +import ( + "github.com/fox/fox/log" + "github.com/nsqio/go-nsq" +) + +type nsqLogger struct { + level nsq.LogLevel +} + +func newNSQLogger(level nsq.LogLevel) *nsqLogger { + return &nsqLogger{level} +} + +func (l *nsqLogger) Output(callDepth int, s string) error { + _ = callDepth + switch l.level { + case nsq.LogLevelDebug: + log.GetLogger().Debug(s) + case nsq.LogLevelInfo: + log.GetLogger().Error(s) + case nsq.LogLevelWarning: + log.GetLogger().Error(s) + case nsq.LogLevelError: + log.GetLogger().Error(s) + default: + log.GetLogger().Error(s) + } + return nil +} diff --git a/nsq/producer.go b/nsq/producer.go new file mode 100644 index 0000000..dbe86f3 --- /dev/null +++ b/nsq/producer.go @@ -0,0 +1,76 @@ +package nsq + +import ( + "errors" + "fmt" + "github.com/nsqio/go-nsq" + "time" +) + +type Producer struct { + nsqAddr []string + config *nsq.Config + producer *nsq.Producer + size int + pos int +} + +func initProducer(p *Producer, nsqAddr string) error { + var err error + p.producer, err = nsq.NewProducer(nsqAddr, p.config) + if err != nil { + return err + } + p.producer.SetLogger(newNSQLogger(nsq.LogLevelError), nsq.LogLevelError) + return nil +} + +// 创建建生产者 nsqAddr:127.0.0.1:4150, 127.0.0.2:4150 +func NewProducer(nsqAddr ...string) (*Producer, error) { + var err error + p := new(Producer) + p.config = nsq.NewConfig() + // 心跳设置 + p.config.HeartbeatInterval = 30 * time.Second + + p.nsqAddr = nsqAddr + p.size = len(nsqAddr) + p.pos = 0 + if len(nsqAddr) == 0 { + return nil, errors.New("nsqAddr is empty") + } + + err = initProducer(p, p.nsqAddr[0]) + return p, err +} + +// 向主题发布消息 +func (p *Producer) Publish(topic string, data []byte) error { + var err error + for { + if p.producer != nil { + err = p.producer.Publish(topic, data) + if err == nil { + break + } + // 切换ip重发 + p.producer.Stop() + } + + p.pos = p.pos + 1 + if p.pos < p.size { + err = initProducer(p, p.nsqAddr[p.pos]) + } else { + p.pos = 0 + return fmt.Errorf("连接nsq:%v 失败", p.nsqAddr) + } + } + return nil +} + +func (p *Producer) Close() { + if p.producer != nil { + p.producer.Stop() + p.producer = nil + } +} diff --git a/rmq/rmq.go b/rmq/rmq.go new file mode 100644 index 0000000..7642226 --- /dev/null +++ b/rmq/rmq.go @@ -0,0 +1,111 @@ +package rmq + +import ( + "context" + "github.com/fox/fox/log" + "github.com/google/uuid" + "github.com/rabbitmq/amqp091-go" + "time" +) + +const ( + ExchangeDirect = "direct" + // ExchangeFanout = "fanout" + ExchangeTopic = "topic" +) + +type Rmq struct { + conn *amqp091.Connection + ch *amqp091.Channel + consumerTag string +} + +// url:amqp://guest:guest@localhost:5672/ +func NewRmq(url string) (*Rmq, error) { + rmq := &Rmq{consumerTag: uuid.NewString()} + + retries := 0 + retryDelay := 1 * time.Second + + var err error + for { + rmq.conn, err = amqp091.DialConfig(url, amqp091.Config{Heartbeat: 10 * time.Second}) + if err == nil { + break + } + retries++ + time.Sleep(retryDelay) + if retryDelay < 30*time.Second { + retryDelay *= 2 + } + log.ErrorF("amqp connection failed after %v reconnect.err:%v url:%v", retryDelay, err, url) + } + + rmq.ch, err = rmq.conn.Channel() + if err != nil { + return nil, err + } + return rmq, nil +} + +func (r *Rmq) Close() { + if r.conn != nil { + _ = r.conn.Close() + } + if r.ch != nil { + _ = r.ch.Close() + } +} + +func (r *Rmq) ExchangeDeclare(exchangeName, typ string) error { + return r.ch.ExchangeDeclare(exchangeName, typ, true, false, false, false, nil) +} + +func (r *Rmq) QueueDeclare(queueName string) error { + _, err := r.ch.QueueDeclare(queueName, false, false, false, false, nil) + return err +} + +func (r *Rmq) QueueDelete(queueName string) error { + _, err := r.ch.QueueDelete(queueName, false, false, false) + return err +} + +func (r *Rmq) QueueBind(queueName, routerKey, exchangeName string) error { + return r.ch.QueueBind(queueName, routerKey, exchangeName, false, nil) +} + +func (r *Rmq) QueueUnbind(queueName, routerKey, exchangeName string) error { + return r.ch.QueueUnbind(queueName, routerKey, exchangeName, nil) +} + +// 发布消息到交换机,带有指定的路由键 +func (r *Rmq) Publish(exchangeName, routerKey string, msg []byte) error { + return r.ch.Publish(exchangeName, routerKey, true, false, amqp091.Publishing{ + ContentType: "text/plain", + Body: msg, + }) +} + +// 发布消息到交换机,带有指定的路由键 +func (r *Rmq) PublishRpc(d *amqp091.Delivery, msg []byte) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return r.ch.PublishWithContext(ctx, "", d.ReplyTo, false, false, amqp091.Publishing{ + ContentType: "text/plain", + CorrelationId: d.CorrelationId, + Body: msg, + }) +} + +func (r *Rmq) PublishRaw(exchangeName, routerKey string, data amqp091.Publishing) error { + return r.ch.Publish(exchangeName, routerKey, true, false, data) +} + +func (r *Rmq) Consume(queueName string) (<-chan amqp091.Delivery, error) { + return r.ch.Consume(queueName, r.consumerTag, true, false, false, false, nil) +} + +func (r *Rmq) ConsumeDelete() error { + return r.ch.Cancel(r.consumerTag, true) +} diff --git a/rmq/rmq_test.go b/rmq/rmq_test.go new file mode 100644 index 0000000..108fb6c --- /dev/null +++ b/rmq/rmq_test.go @@ -0,0 +1,53 @@ +package rmq + +import ( + "testing" + "time" +) + +const url = "amqp://samba:samba@testbuild.shoa.com:5672/vh_samba" +const exchangeName = "test_e" +const queueName = "test_q" + +func testCreateMq(t *testing.T) *Rmq { + mq, err := NewRmq(url) + if err != nil { + t.Error(err) + } + err = mq.ExchangeDeclare(exchangeName, ExchangeDirect) + if err != nil { + t.Error(err) + } + err = mq.QueueDeclare(queueName) + if err != nil { + t.Error(err) + } + return mq +} + +func testPublish(t *testing.T, mq *Rmq) { + err := mq.Publish(exchangeName, queueName, []byte("hello world")) + if err != nil { + t.Error(err) + } + return +} + +func testConsume(t *testing.T, mq *Rmq) { + msgs, err := mq.Consume(queueName) + if err != nil { + t.Error(err) + } + for msg := range msgs { + t.Log(string(msg.Body)) + } + return +} + +func TestRabbitmq(t *testing.T) { + mq := testCreateMq(t) + defer mq.Close() + go testConsume(t, mq) + testPublish(t, mq) + time.Sleep(2 * time.Second) +} diff --git a/safeChan/safeChan.go b/safeChan/safeChan.go new file mode 100644 index 0000000..d9dc645 --- /dev/null +++ b/safeChan/safeChan.go @@ -0,0 +1,54 @@ +package safeChan + +import ( + "context" + "fmt" + "sync" +) + +type ByteChan = SafeChan[[]byte] + +type SafeChan[T any] struct { + ch chan T + ctx context.Context + cancel context.CancelFunc + once sync.Once +} + +func NewSafeChan[T any](size int) *SafeChan[T] { + ch := &SafeChan[T]{} + ch.ctx, ch.cancel = context.WithCancel(context.Background()) + if size < 1 { + ch.ch = make(chan T) + } else { + ch.ch = make(chan T, size) + } + // ch.ch = make(chan T, size) + return ch +} + +func (s *SafeChan[T]) Close() { + s.once.Do(func() { + s.cancel() + close(s.ch) + }) +} + +// 管道中剩余数量 +func (s *SafeChan[T]) Size() int { + return len(s.ch) +} + +func (s *SafeChan[T]) Reader() <-chan T { + return s.ch +} + +func (s *SafeChan[T]) Write(d T) error { + select { + case <-s.ctx.Done(): + return fmt.Errorf("chan was closed") + default: + s.ch <- d + return nil + } +} diff --git a/safeChan/safeChan_test.go b/safeChan/safeChan_test.go new file mode 100644 index 0000000..8bd7e5e --- /dev/null +++ b/safeChan/safeChan_test.go @@ -0,0 +1,43 @@ +package safeChan + +import ( + "testing" +) + +func TestSafeChan(t *testing.T) { + ch := NewSafeChan[string](12) + + // go func() { + _ = ch.Write("hello") + t.Log("write hello. 剩余数量:", ch.Size()) + // }() + ch.Close() + if err := ch.Write("zzz"); err != nil { + t.Log("write zzz err.", err) + } + + breakNum := 0 + for { + + select { + case <-ch.ctx.Done(): + t.Log("done") + breakNum++ + case v, ok := <-ch.Reader(): + if ok { + t.Log("read", v, " 剩余数量:", ch.Size()) + } else { + t.Log("break") + breakNum++ + } + + default: + t.Log("panic") + breakNum++ + } + if breakNum > 10 { + break + } + } + +} diff --git a/service/baseService.go b/service/baseService.go new file mode 100644 index 0000000..23dc8d4 --- /dev/null +++ b/service/baseService.go @@ -0,0 +1,182 @@ +package service + +import ( + "context" + "fmt" + "github.com/fox/fox/ksync" + "github.com/fox/fox/log" + "github.com/fox/fox/safeChan" + "github.com/fox/fox/timer" + "time" +) + +type BaseService struct { + *timer.Timer + type_ string + name string + + onFunc IOnFunc + sender ISender + + msg *safeChan.SafeChan[[]byte] + job *safeChan.SafeChan[func()] + stop context.Context + stopFunc context.CancelFunc + waitStop context.Context + waitStopFunc context.CancelFunc +} + +func NewBaseService(type_, name string, onFunc IOnFunc, sender ISender) *BaseService { + s := &BaseService{ + type_: type_, + name: name, + Timer: timer.NewTimer(), + onFunc: onFunc, + sender: sender, + + msg: safeChan.NewSafeChan[[]byte](128), + job: safeChan.NewSafeChan[func()](128), + } + s.stop, s.stopFunc = context.WithCancel(context.Background()) + s.waitStop, s.waitStopFunc = context.WithCancel(context.Background()) + + s.Run() + return s +} + +func (s *BaseService) Name() string { + return s.name +} + +func (s *BaseService) Type() string { + return s.type_ +} + +func (s *BaseService) Write(msg []byte) error { + return s.msg.Write(msg) +} + +func (s *BaseService) RunOnce(cb func()) { + select { + case <-s.stop.Done(): + log.Error(s.Log("want stop, can not call RunOnce function")) + return + default: + _ = s.job.Write(cb) + } +} + +func (s *BaseService) RunWait(cb func() (retValue any)) (retValue any, err error) { + select { + case <-s.stop.Done(): + err = fmt.Errorf(s.Log("want stop, can not call RunOnce function")) + log.Error(err.Error()) + return nil, err + default: + wait := make(chan any, 2) + err = s.job.Write(func() { + retValue = cb() + wait <- retValue + }) + if err == nil { + select { + case retValue = <-wait: + return retValue, nil + case <-time.After(time.Second * time.Duration(30)): + return nil, fmt.Errorf("timeout fail") + } + } + return nil, err + } +} + +func (s *BaseService) Send(topic string, msg []byte) error { + if s.sender != nil { + return s.sender.Send(topic, msg) + } + return s.Err("send is nil") +} +func (s *BaseService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) { + if s.sender != nil { + return s.sender.Call(topic, timeout, msg) + } + return nil, s.Err("call is nil") +} + +func (s *BaseService) WaitStop() { + select { + case <-s.waitStop.Done(): + return + } +} + +func (s *BaseService) NotifyStop() { + s.stopFunc() + // log.Debug(fmt.Sprintf("notify %v service stop", s.name)) +} + +func (s *BaseService) allChanEmpty() bool { + if s.job.Size() == 0 && s.msg.Size() == 0 { + return true + } + return false +} + +func (s *BaseService) canStop() bool { + select { + case <-s.stop.Done(): + if s.allChanEmpty() { + return true + } + return false + default: + return false + } +} + +func (s *BaseService) run() { + for { + if s.canStop() { + if s.onFunc != nil { + s.onFunc.OnStop() + s.waitStopFunc() + } + break + } + select { + case msg, ok := <-s.msg.Reader(): + if ok && s.onFunc != nil { + _ = s.onFunc.OnMessage(msg) + } + case cb, ok := <-s.job.Reader(): + if ok && cb != nil { + cb() + } + case t, ok := <-s.Timer.Reader(): + if ok && t != nil && t.Func != nil { + t.Func() + } + case _ = <-s.stop.Done(): + if s.onFunc != nil { + s.msg.Close() + s.job.Close() + s.Timer.CancelAllTimer() + s.Timer.Close() + } + } + } +} + +func (s *BaseService) Run() { + ksync.GoSafe(s.run, s.Run) +} + +func (s *BaseService) Log(format string, a ...any) string { + head := fmt.Sprintf("service:%v-%v ", s.type_, s.name) + return head + fmt.Sprintf(format, a...) +} + +func (s *BaseService) Err(format string, a ...any) error { + head := fmt.Sprintf("service:%v-%v ", s.type_, s.name) + return fmt.Errorf(head + fmt.Sprintf(format, a...)) +} diff --git a/service/iservice.go b/service/iservice.go new file mode 100644 index 0000000..99f6074 --- /dev/null +++ b/service/iservice.go @@ -0,0 +1,35 @@ +package service + +import ( + "time" +) + +type IService interface { + Name() string + Type() string + RunOnce(cb func()) + RunWait(cb func() (retValue any)) (retValue any, err error) + + NewTimer(duration time.Duration, cb func(), needLog bool, desc ...string) uint32 + CancelTimer(timerId uint32) + CancelAllTimer() + + // 向服务内部消息管道写入消息 + Write(msg []byte) error + Send(topic string, msg []byte) error + Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) + + WaitStop() + NotifyStop() +} + +type ISender interface { + Send(topic string, msg []byte) error + Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) +} + +type IOnFunc interface { + OnStop() + OnInit() + OnMessage(msg []byte) error +} diff --git a/service/natsService.go b/service/natsService.go new file mode 100644 index 0000000..27096b7 --- /dev/null +++ b/service/natsService.go @@ -0,0 +1,48 @@ +package service + +import ( + "fmt" + "github.com/fox/fox/nat" + "time" +) + +type NatsService struct { + *BaseService + nats *nat.Nats +} + +func NewNatsService(type_, name string, onFunc IOnFunc, natsAddress ...string) (*NatsService, error) { + s := new(NatsService) + s.BaseService = NewBaseService(type_, name, onFunc, s) + s.nats = nat.NewNats(fmt.Sprintf("%v-%v", type_, name), natsAddress...) + if err := s.nats.Connect(); err != nil { + // log.Error(err.Error()) + s.BaseService.NotifyStop() + return nil, err + } + return s, nil +} + +func (n *NatsService) Subscribe(topic string) error { + return n.nats.Subscribe(topic, n.msg) +} + +// 队列订阅,队列中只会有一个消费者消费该消息 +func (n *NatsService) QueueSubscribe(topic string, queue string) error { + return n.nats.QueueSubscribe(topic, queue, n.msg) +} + +func (s *NatsService) OnStop() { + s.nats.Close() +} + +func (s *NatsService) Send(topic string, msg []byte) error { + return s.nats.Publish(topic, msg) +} + +func (s *NatsService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) { + _ = topic + _ = timeout + _ = msg + return nil, nil +} diff --git a/service/natsService_test.go b/service/natsService_test.go new file mode 100644 index 0000000..52bd299 --- /dev/null +++ b/service/natsService_test.go @@ -0,0 +1,97 @@ +package service + +import ( + "github.com/fox/fox/etcd" + "github.com/fox/fox/log" + "testing" + "time" +) + +const ( + GameSrv = "game" + NatsAddress = "nats://192.168.232.128:4222" + EtcdAddress = "192.168.232.128:2379" + EtcdAddress2 = "114.132.124.145:2379" + NatsAddress2 = "nats://114.132.124.145:4222" +) + +func newGameService() *gameService { + _ = NatsAddress2 + _ = NatsAddress + _ = EtcdAddress2 + _ = EtcdAddress + + var err error + s := new(gameService) + if s.NatsService, err = NewNatsService(GameSrv, "1", s, NatsAddress2); err != nil { + log.Fatal(err.Error()) + return nil + } + if s.etcdService, err = etcd.NewRegistry[etcd.ServiceNode]([]string{EtcdAddress2}, etcd.ServiceNode{}.EtcdRootKey(), "", ""); err != nil { + log.Error(err.Error()) + s.NatsService.OnStop() + return nil + } + endpoint := &etcd.ServiceNode{ + Name: s.Name(), + Type: s.Type(), + Address: "", + Port: 0, + Version: "", + ServiceType: etcd.Unique, + } + if err = s.etcdService.Register(endpoint); err != nil { + log.Error(err.Error()) + s.NatsService.OnStop() + return nil + } + s.OnInit() + return s +} + +type gameService struct { + *NatsService + etcdService *etcd.Registry[etcd.ServiceNode] + etcdTopic *etcd.Registry[etcd.TopicNode] + srvTopic string +} + +func (s *gameService) OnInit() { + s.etcdService.WatchServices() + s.etcdTopic.WatchServices() + if err := s.NatsService.Subscribe(Topic(s)); err != nil { + log.Error(err.Error()) + } + if err := s.NatsService.QueueSubscribe(GroupTopic(s), GroupQueue(s)); err != nil { + log.Error(err.Error()) + } + log.Debug("onInit") +} + +func (s *gameService) OnStop() { + s.etcdService.UnregisterService() + s.etcdService.UnregisterService() + s.NatsService.OnStop() + log.Debug("OnStop") +} + +func (s *gameService) OnMessage(msg []byte) error { + log.Debug(s.Log("on message:%v", string(msg))) + return nil +} + +func TestGameService(t *testing.T) { + log.Open("test.log", log.DebugL) + s := newGameService() + msg := "hello world" + if err := s.Send(Topic(s), []byte(msg)); err != nil { + log.Error(err.Error()) + } + for _, srv := range s.etcdService.GetNodes() { + log.Debug(s.Log("发现有服务:%v", srv)) + } + time.Sleep(1 * time.Second) + s.NotifyStop() + s.WaitStop() + log.Debug("exit") +} diff --git a/service/service_test.go b/service/service_test.go new file mode 100644 index 0000000..70b8651 --- /dev/null +++ b/service/service_test.go @@ -0,0 +1,61 @@ +package service + +import ( + "github.com/fox/fox/log" + "testing" + "time" +) + +const ( + TestSrv = "test" +) + +type EchoService struct { + *BaseService +} + +func (s *EchoService) OnInit() { + log.Debug("onInit") +} + +func (s *EchoService) OnStop() { + log.Debug("OnStop") +} + +func (s *EchoService) Send(topic string, msg []byte) error { + log.Debug(s.Log("send %v to topic:%v", string(msg), topic)) + return nil +} + +func (s *EchoService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) { + _ = timeout + _ = msg + log.Debug(s.Log("call topic:%v", topic)) + return nil, nil +} + +func (s *EchoService) OnMessage(msg []byte) error { + log.Debug(s.Log("on message:%v", string(msg))) + return nil +} + +func NewTestService() *EchoService { + s := new(EchoService) + s.BaseService = NewBaseService(TestSrv, "1", s, s) + s.OnInit() + return s +} + +func TestService(t *testing.T) { + log.Open("test.log", log.DebugL) + s := NewTestService() + msg := "hello world" + _ = s.Write([]byte(msg)) + s.RunOnce(func() { + time.Sleep(2 * time.Second) + }) + + s.NotifyStop() + s.WaitStop() + log.Debug("exit") +} diff --git a/service/topic.go b/service/topic.go new file mode 100644 index 0000000..589ed47 --- /dev/null +++ b/service/topic.go @@ -0,0 +1,18 @@ +package service + +import ( + "fmt" +) + +// 每个服务都有自己的服务topic +func Topic(s IService) string { + return fmt.Sprintf("%v-%v.topic", s.Type(), s.Name()) +} + +func GroupTopic(s IService) string { + return s.Type() + ".topic" +} + +func GroupQueue(s IService) string { + return s.Type() + ".group" +} diff --git a/timer/timer.go b/timer/timer.go new file mode 100644 index 0000000..f7a31c0 --- /dev/null +++ b/timer/timer.go @@ -0,0 +1,108 @@ +package timer + +import ( + "github.com/fox/fox/log" + "github.com/fox/fox/safeChan" + cmap "github.com/orcaman/concurrent-map/v2" + "math" + "sync/atomic" + "time" +) + +type timerData struct { + timer *time.Timer + id uint32 + Func func() + desc string + needLog bool +} + +type Timer struct { + chTimer *safeChan.SafeChan[*timerData] + timers cmap.ConcurrentMap[uint32, *timerData] + no uint32 + stop int32 +} + +func NewTimer() *Timer { + rt := &Timer{} + rt.chTimer = safeChan.NewSafeChan[*timerData](16) + rt.timers = cmap.NewWithCustomShardingFunction[uint32, *timerData](func(key uint32) uint32 { return key }) + rt.no = math.MaxUint32 - 2 + rt.stop = 0 + return rt +} + +func (rt *Timer) NewTimer(duration time.Duration, cb func(), needLog bool, desc ...string) uint32 { + tData := &timerData{ + timer: nil, + id: atomic.AddUint32(&rt.no, 1), + Func: cb, + needLog: needLog, + } + if len(desc) > 0 { + tData.desc = desc[0] + } + t := time.AfterFunc(duration, func() { + _ = rt.chTimer.Write(tData) + rt.timers.Remove(tData.id) + if needLog { + if tData.desc != "" { + log.DebugF("移除定时器:%v NewTimer desc:%v time:%v", tData.id, tData.desc, duration) + } else { + log.DebugF("移除定时器:%v NewTimer", tData.id) + } + } + }) + tData.timer = t + rt.timers.Set(tData.id, tData) + if needLog { + if tData.desc != "" { + log.DebugF("设置定时器:%v NewTimer desc:%v", tData.id, tData.desc) + } else { + log.DebugF("设置定时器:%v NewTimer", tData.id) + } + } + + return tData.id +} + +func (rt *Timer) CancelTimer(timerId uint32) { + if t, _ := rt.timers.Get(timerId); t != nil { + if t.timer != nil { + _ = t.timer.Stop() + } + rt.timers.Remove(timerId) + if t.needLog { + if t.desc != "" { + log.DebugF("移除定时器:%v CancelTimer desc:%v", t.id, t.desc) + } else { + log.DebugF("移除定时器:%v CancelTimer", t.id) + } + } + } +} + +func (rt *Timer) CancelAllTimer() { + rt.timers.IterCb(func(_ uint32, t *timerData) { + if t.timer != nil { + t.timer.Stop() + if t.needLog { + if t.desc != "" { + log.DebugF("移除定时器:%v CancelAllTimer desc:%v", t.id, t.desc) + } else { + log.DebugF("移除定时器:%v CancelAllTimer", t.id) + } + } + } + }) + rt.timers.Clear() +} + +func (rt *Timer) Reader() <-chan *timerData { + return rt.chTimer.Reader() +} + +func (rt *Timer) Close() { + rt.chTimer.Close() +} diff --git a/timer/timer_test.go b/timer/timer_test.go new file mode 100644 index 0000000..ec0df80 --- /dev/null +++ b/timer/timer_test.go @@ -0,0 +1,51 @@ +package timer + +import ( + "github.com/fox/fox/ksync" + "testing" + "time" +) + +func runTimerEvent(tm *Timer) { + ksync.GoSafe(func() { + for { + select { + case t, ok := <-tm.chTimer.Reader(): + if ok && t != nil && t.Func != nil { + t.Func() + } + } + } + }, nil) +} + +func TestTimer(t *testing.T) { + timer := NewTimer() + runTimerEvent(timer) + id := timer.NewTimer(1*time.Second, func() { + t.Log("this is timer1") + }, false) + t.Log("new timer:", id) + + id = timer.NewTimer(2*time.Second, func() { + t.Log("this is timer2") + }, false) + t.Log("new timer:", id) + + id = timer.NewTimer(3*time.Second, func() { + t.Log("this is timer3") + }, false) + t.Log("new timer:", id) + + id = timer.NewTimer(1*time.Second, func() { + t.Log("this is timer4") + }, false) + t.Log("new timer:", id) + + timer.CancelTimer(id) + t.Log("cancel timer:", id) + + // fmt.Println("rrr id:", id) + time.Sleep(5 * time.Second) + t.Log("rest timer:", timer.timers.Count(), " count") +} diff --git a/ws/iconn.go b/ws/iconn.go new file mode 100644 index 0000000..310edde --- /dev/null +++ b/ws/iconn.go @@ -0,0 +1,10 @@ +package ws + +type IConn interface { + Close() + SendMsg(data []byte) error + Name() string + Id() uint32 + UserId() int64 + Log(format string, v ...interface{}) string +} diff --git a/ws/userMgr.go b/ws/userMgr.go new file mode 100644 index 0000000..5697746 --- /dev/null +++ b/ws/userMgr.go @@ -0,0 +1,41 @@ +package ws + +import cmap "github.com/orcaman/concurrent-map/v2" + +var userMgr = newUserManager() + +type userManager struct { + users cmap.ConcurrentMap[int64, uint32] +} + +func newUserManager() *userManager { + return &userManager{ + users: cmap.NewWithCustomShardingFunction[int64, uint32](func(key int64) uint32 { + return uint32(key) + }), + } +} + +func (m *userManager) Add(connId uint32, userId int64) bool { + if userId < 1 { + return false + } + if conn, ok := wsMgr.Get(connId); ok { + conn.setUserId(userId) + m.users.Set(userId, connId) + return true + } + return false +} + +func (m *userManager) GetConnId(userId int64) uint32 { + connId, _ := m.users.Get(userId) + return connId +} + +func (m *userManager) Remove(userId int64) { + if userId < 1 { + return + } + m.users.Remove(userId) +} diff --git a/ws/wsClient.go b/ws/wsClient.go new file mode 100644 index 0000000..bf25924 --- /dev/null +++ b/ws/wsClient.go @@ -0,0 +1,125 @@ +package ws + +import ( + "context" + "github.com/fox/fox/ksync" + "github.com/fox/fox/log" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +type Client struct { + conn *websocket.Conn + sendChan chan *wsMessage + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +func NewClient(url string) (*Client, error) { + dialer := websocket.DefaultDialer + dialer.HandshakeTimeout = 30 * time.Second + conn, _, err := dialer.Dial(url, http.Header{"User-Agent": {"MyClient/1.0"}}) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + + return &Client{ + conn: conn, + sendChan: make(chan *wsMessage, 100), + ctx: ctx, + cancel: cancel, + }, nil +} + +func (c *Client) Start() { + c.wg.Add(3) + ksync.GoSafe(c.readLoop, nil) + ksync.GoSafe(c.writeLoop, nil) + ksync.GoSafe(c.heartbeatLoop, nil) +} + +func (c *Client) readLoop() { + defer c.wg.Done() + for { + select { + case <-c.ctx.Done(): + return + default: + messageType, message, err := c.conn.ReadMessage() + if err != nil { + // log.Error(fmt.Sprintf("读取错误:%v", err)) + c.Stop() + return + } + + switch messageType { + case websocket.PingMessage: + c.sendChan <- &wsMessage{messageType: websocket.PongMessage, data: []byte("pong")} + case websocket.PongMessage: + + case websocket.TextMessage, websocket.BinaryMessage: + log.DebugF("收到消息,类型:%v 内容:%v", messageType, string(message)) + case websocket.CloseMessage: + log.Debug("收到关闭帧") + c.Stop() + return + } + } + } +} + +func (c *Client) SendMsg(data []byte) { + c.sendChan <- &wsMessage{messageType: websocket.BinaryMessage, data: data} +} + +func (c *Client) writeLoop() { + defer c.wg.Done() + for { + select { + case msg := <-c.sendChan: + switch msg.messageType { + case websocket.PingMessage: + _ = c.conn.WriteMessage(websocket.PingMessage, []byte("ping")) + case websocket.PongMessage: + _ = c.conn.WriteMessage(websocket.PongMessage, []byte("pong")) + default: + _ = c.conn.WriteMessage(msg.messageType, msg.data) + } + case <-c.ctx.Done(): + // 发送关闭帧 + _ = c.conn.WriteControl( + websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), + time.Now().Add(10*time.Second), + ) + return + } + } +} + +func (c *Client) heartbeatLoop() { + defer c.wg.Done() + ticker := time.NewTicker(25 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.sendChan <- &wsMessage{messageType: websocket.PingMessage, data: []byte("ping")} + case <-c.ctx.Done(): + return + } + } +} + +func (c *Client) Stop() { + c.cancel() + _ = c.conn.Close() + c.wg.Wait() +} diff --git a/ws/wsConn.go b/ws/wsConn.go new file mode 100644 index 0000000..c5854c4 --- /dev/null +++ b/ws/wsConn.go @@ -0,0 +1,194 @@ +package ws + +import ( + "encoding/binary" + "fmt" + "github.com/fox/fox/log" + "github.com/gorilla/websocket" + "sync" + "time" +) + +// 连接id +var ( + wsMsgType = websocket.BinaryMessage + nextConnId uint32 +) + +// 客户端读写消息 +type wsMessage struct { + messageType int + data []byte +} + +// 客户端连接 +type wsConnect struct { + wsConn *websocket.Conn // 底层websocket + inChan chan *wsMessage // 读队列 + outChan chan *wsMessage // 写队列 + mutex sync.Mutex // 避免重复关闭管道,加锁处理 + isClosed bool + closeCh chan struct{} // 关闭通知 + id uint32 + userId int64 + onDisconnect func(IConn) +} + +func newWsConnect(wsConn *websocket.Conn, onDisconnect func(IConn)) *wsConnect { + return &wsConnect{ + wsConn: wsConn, + inChan: make(chan *wsMessage, 1000), + outChan: make(chan *wsMessage, 1000), + closeCh: make(chan struct{}), + isClosed: false, + id: nextConnId, + userId: 0, + onDisconnect: onDisconnect, + } +} + +// 从读队列读取消息 +func (c *wsConnect) readFromChan() (*wsMessage, error) { + select { + case msg := <-c.inChan: + return msg, nil + case <-c.closeCh: + return nil, fmt.Errorf("连接已关闭") + } +} + +// 把消息放进写队列 +func (c *wsConnect) sendMsg(msgType int, data []byte) error { + select { + case c.outChan <- &wsMessage{messageType: msgType, data: data}: + case <-c.closeCh: + return fmt.Errorf("连接已关闭") + } + return nil +} + +// 把消息放进写队列 +func (c *wsConnect) SendMsg(data []byte) error { + return c.sendMsg(wsMsgType, data) +} + +// 关闭链接 +func (c *wsConnect) Close() { + log.Debug(c.Log("关闭链接")) + c.mutex.Lock() + defer c.mutex.Unlock() + if c.isClosed == false { + c.isClosed = true + wsMgr.Remove(c) + close(c.closeCh) + if c.onDisconnect != nil { + c.onDisconnect(c) + } + } +} + +// 循环从websocket中读取消息放入到读队列中 +func (c *wsConnect) readWsLoop() { + c.wsConn.SetReadLimit(maxMessageSize) + _ = c.wsConn.SetReadDeadline(time.Now().Add(pongWait)) + for { + // 读一个message + msgType, data, err := c.wsConn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { + log.Error(c.Log("消息读取出现错误:%v", err)) + } + c.Close() + return + } + switch msgType { + case websocket.PingMessage: + _ = c.sendMsg(websocket.PongMessage, []byte("pong")) + case websocket.PongMessage: + // _ = c.sendMsg(websocket.PingMessage, []byte("ping")) + case websocket.CloseMessage: + code := websocket.CloseNormalClosure + reason := "" + if len(data) >= 2 { + code = int(binary.BigEndian.Uint16(data)) + reason = string(data[2:]) + } + log.Debug(c.Log("关闭原因码:%d 描述:%s", code, reason)) + // 发送响应关闭帧(必须回传相同状态码) + rspMsg := websocket.FormatCloseMessage(code, reason) + _ = c.wsConn.WriteControl(websocket.CloseMessage, rspMsg, time.Now().Add(5*time.Second)) + c.Close() + default: + if msgType != wsMsgType { + continue + } + msg := &wsMessage{messageType: msgType, data: data} + select { + case c.inChan <- msg: + case <-c.closeCh: + return + } + } + } +} + +func (c *wsConnect) writeWsLoop() { + ticker := time.NewTicker(pingPeriod) + defer ticker.Stop() + for { + select { + // 取一个消息发送给客户端 + case msg := <-c.outChan: + if err := c.wsConn.WriteMessage(msg.messageType, msg.data); err != nil { + log.Error(c.Log("发送消息错误:%v", err)) + // 关闭连接 + c.Close() + return + } + case <-c.closeCh: + // 收到关闭通知 + return + case <-ticker.C: + _ = c.wsConn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.wsConn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func (c *wsConnect) handle(process func(IConn, []byte)) { + for { + msg, err := c.readFromChan() + if err != nil { + // log.Error(c.Log("获取消息错误:%v", err)) + break + } + // Log.Debug(c.Log("接收消息:%v", msg.data)) + process(c, msg.data) + } +} + +// 设置用户id +func (c *wsConnect) setUserId(uid int64) { + c.userId = uid +} + +// 获取连接id +func (c *wsConnect) Id() uint32 { + return c.id +} + +// 获取用户id +func (c *wsConnect) UserId() int64 { + return c.userId +} + +func (c *wsConnect) Name() string { + return fmt.Sprintf("用户:%v, 地址:%v", c.userId, c.wsConn.RemoteAddr()) +} + +func (c *wsConnect) Log(format string, v ...interface{}) string { + s := fmt.Sprintf("连接:%v, id:%v ", c.wsConn.RemoteAddr().String(), c.id) + return s + fmt.Sprintf(format, v...) +} diff --git a/ws/wsMgr.go b/ws/wsMgr.go new file mode 100644 index 0000000..36b737c --- /dev/null +++ b/ws/wsMgr.go @@ -0,0 +1,45 @@ +package ws + +import cmap "github.com/orcaman/concurrent-map/v2" + +var wsMgr = newManager() + +type wsManager struct { + wsConnAll cmap.ConcurrentMap[uint32, *wsConnect] +} + +func newManager() *wsManager { + return &wsManager{ + wsConnAll: cmap.NewWithCustomShardingFunction[uint32, *wsConnect](func(key uint32) uint32 { + return key + }), + } +} + +func (m *wsManager) Add(conn *wsConnect) { + m.wsConnAll.Set(conn.id, conn) +} + +func (m *wsManager) SetUserId(connId uint32, userId int64) { + userMgr.Add(connId, userId) +} + +func (m *wsManager) Remove(conn *wsConnect) { + if conn.UserId() > 0 { + userMgr.Remove(conn.UserId()) + } + m.wsConnAll.Remove(conn.id) +} + +func (m *wsManager) Get(connId uint32) (*wsConnect, bool) { + return m.wsConnAll.Get(connId) +} + +func (m *wsManager) FindByUserId(userId int64) (*wsConnect, bool) { + connId := userMgr.GetConnId(userId) + return m.wsConnAll.Get(connId) +} + +func (m *wsManager) Count() int { + return m.wsConnAll.Count() +} diff --git a/ws/wsServer.go b/ws/wsServer.go new file mode 100644 index 0000000..cf6db00 --- /dev/null +++ b/ws/wsServer.go @@ -0,0 +1,81 @@ +package ws + +import ( + "github.com/fox/fox/ksync" + "github.com/fox/fox/log" + "github.com/gorilla/websocket" + "net/http" + "time" +) + +const ( + // 允许等待的写入时间 + writeWait = 10 * time.Second + // pong间隔时间 + pongWait = 60 * time.Second + // ping间隔时间 + pingPeriod = (pongWait * 9) / 10 + // 最大消息长度 + maxMessageSize = 512 +) + +var upGrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // 允许所有跨域请求(生产环境应限制) + // 跨域限制 + // allowedOrigins := map[string]bool{ + // "https://yourdomain.com": true, + // "https://api.yourdomain.com": true, + // } + // return allowedOrigins[r.Header.Get("Origin")] + }, + HandshakeTimeout: 10 * time.Second, // 握手超时 + ReadBufferSize: 4096, // 读缓冲区 + WriteBufferSize: 4096, // 写缓冲区 + // MaxMessageSize: 1024 * 1024 * 2, // 最大消息2MB +} + +type WsServer struct { + addr string // 0.0.0.0:8888 + onMessage func(IConn, []byte) + onDisconnect func(IConn) +} + +func NewWsServer(addr string, onMessage func(IConn, []byte), onDisconnect func(IConn)) *WsServer { + return &WsServer{addr: addr, onMessage: onMessage, onDisconnect: onDisconnect} +} + +func (s *WsServer) wsHandle(w http.ResponseWriter, r *http.Request) { + conn, err := upGrader.Upgrade(w, r, nil) + if err != nil { + log.ErrorF("升级到WebSocket失败:%v", err) + return + } + // defer func() { _ = conn.Close() }() + nextConnId++ + wsConn := newWsConnect(conn, s.onDisconnect) + wsMgr.Add(wsConn) + log.DebugF("当前在线人数:%v", wsMgr.Count()) + ksync.GoSafe(func() { wsConn.handle(s.onMessage) }, nil) + ksync.GoSafe(wsConn.readWsLoop, nil) + ksync.GoSafe(wsConn.writeWsLoop, nil) +} + +func (s *WsServer) Run() { + http.HandleFunc("/", s.wsHandle) + log.DebugF("websocket server listening on :%v", s.addr) + ksync.GoSafe(func() { + err := http.ListenAndServe(s.addr, nil) + if err != nil { + log.Error(err.Error()) + } + }, nil) +} + +func (s *WsServer) SetUserId(connId uint32, userId int64) { + wsMgr.SetUserId(connId, userId) +} + +func (s *WsServer) FindConnByUserId(userId int64) (IConn, bool) { + return wsMgr.FindByUserId(userId) +} diff --git a/ws/ws_test.go b/ws/ws_test.go new file mode 100644 index 0000000..519f01a --- /dev/null +++ b/ws/ws_test.go @@ -0,0 +1,66 @@ +package ws + +import ( + "fmt" + "github.com/fox/fox/log" + "testing" + "time" +) + +var ( + serverAddr = ":8080" + addr = "localhost:8080" +) + +func initLog() { + log.Open("test.Log", log.DebugL) +} + +func wsServer() { + s := NewWsServer(serverAddr, func(conn IConn, data []byte) { + log.DebugF("服务端收到消息:%v", string(data)) + _ = conn.SendMsg(data) + }, func(conn IConn) { + log.Debug(conn.Log("退出")) + }) + s.Run() +} + +func wsClient() { + client, err := NewClient(fmt.Sprintf("ws://%v", addr)) + if err != nil { + log.Fatal(err.Error()) + return + } + defer client.Stop() + + count := 1 + client.Start() + msg := fmt.Sprintf("hell world %v", count) + // Log.Debug(fmt.Sprintf("%v", []byte(msg))) + client.SendMsg([]byte(msg)) + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + count++ + client.SendMsg([]byte(fmt.Sprintf("hell world %v", count))) + if count > 30 { + return + } + } + break + } +} + +func TestWebsocket(t *testing.T) { + initLog() + wsServer() + wsClient() + + // time.Sleep(60 * time.Second) + log.Debug("shutdown") +} diff --git a/xrand/rand.go b/xrand/rand.go new file mode 100644 index 0000000..9c488d0 --- /dev/null +++ b/xrand/rand.go @@ -0,0 +1,67 @@ +package xrand + +import ( + "math/rand" + "time" +) + +func init() { + rand.New(rand.NewSource(time.Now().UnixNano())) +} + +var Int = rand.Int + +func Intn(n int) int { + return rand.Intn(n) +} + +func Int31() int32 { + return rand.Int31() +} + +func Int31n(n int32) int32 { + return rand.Int31n(n) +} + +func Int63() int64 { + return rand.Int63() +} + +func Int63n(n int64) int64 { + return rand.Int63n(n) +} + +func Uint32() uint32 { + return rand.Uint32() +} + +func Uint64() uint64 { + return rand.Uint64() +} + +func Float32() float32 { + return rand.Float32() +} + +func Float64() float64 { + return rand.Float64() +} + +func Perm(n int) []int { + return rand.Perm(n) +} + +func Read(p []byte) (n int, err error) { + return rand.Read(p) +} + +func Shuffle[T any](slice []T) { + rand.Shuffle(len(slice), func(i, j int) { + slice[i], slice[j] = slice[j], slice[i] + }) +} + +// RandomInt64 随机生成一个[min, max]之间的整数 +func RandomInt64(min, max int64) int64 { + return Int63n(max-min) + min +} diff --git a/xrand/rand_test.go b/xrand/rand_test.go new file mode 100644 index 0000000..d3100aa --- /dev/null +++ b/xrand/rand_test.go @@ -0,0 +1,9 @@ +package xrand + +import "testing" + +func TestShuffle(t *testing.T) { + a := []int{1, 2, 3} + Shuffle(a) + t.Log(a) +} diff --git a/xrand/weight.go b/xrand/weight.go new file mode 100644 index 0000000..d4d9f00 --- /dev/null +++ b/xrand/weight.go @@ -0,0 +1,42 @@ +package xrand + +import "errors" + +var ErrWeightRandomBadParam = errors.New("WeightRandom: bad param") + +type randomItem[T any] struct { + Weight uint + Data T +} + +// WeightRandom 加权随机 +func WeightRandom[W int | uint | int32 | uint32 | int64 | uint64, T any](m map[W]T) (result T, err error) { + sum := uint(0) + var items []randomItem[T] + for weight, data := range m { + sum += uint(weight) + items = append(items, randomItem[T]{ + Weight: uint(weight), + Data: data, + }) + } + + if len(items) == 0 || sum == 0 { + err = ErrWeightRandomBadParam + return + } + if len(items) == 1 { + return items[0].Data, nil + } + + r := Intn(int(sum)) + for _, item := range items { + r -= int(item.Weight) + if r < 0 { + return item.Data, nil + } + } + + err = ErrWeightRandomBadParam + return +} diff --git a/xrand/weight_test.go b/xrand/weight_test.go new file mode 100644 index 0000000..36b6392 --- /dev/null +++ b/xrand/weight_test.go @@ -0,0 +1,25 @@ +package xrand + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestWeightRandom(t *testing.T) { + stat := make(map[any]int) + for i := 1; i <= 10000; i++ { + items := map[uint]string{ + 5: "a", + 15: "b", + 30: "c", + 50: "d", + } + item, err := WeightRandom(items) + assert.Nil(t, err) + if _, ok := stat[item]; !ok { + stat[item] = 0 + } + stat[item]++ + } + t.Log(stat) +} diff --git a/xtime/constant.go b/xtime/constant.go new file mode 100644 index 0000000..1c67df0 --- /dev/null +++ b/xtime/constant.go @@ -0,0 +1,42 @@ +package xtime + +import ( + "github.com/golang-module/carbon/v2" + "time" +) + +type TimezoneOffset = int + +// 系统时区偏移量,也就是默认时区偏移量 +var sysTimezoneOffset = SaoPauloTimezoneOffset + +var nowOffset int + +const ( + // SaoPauloTimezoneOffset 南美/圣保罗 西三区时间,UTC-3 + SaoPauloTimezoneOffset TimezoneOffset = -3 * 3600 + // ShangHaiTimezoneOffset 亚洲/上海 东八区时间,UTC+3 + ShangHaiTimezoneOffset TimezoneOffset = 8 * 3600 +) + +const ( + + // 每周起始日 + WeekStartsAt = carbon.Monday + + // 时间间隔 + NoDuration = time.Duration(0) + Day = time.Hour * 24 + Week = 7 * Day + Month = 30 * Day +) + +// SetSysTimezoneOffset 设置系统时区偏移量,影响所有默认行为 +func SetSysTimezoneOffset(offset int) { + sysTimezoneOffset = offset +} + +// SetNowTimeOffset 设置当前时间偏移,影响Now的结果 +func SetNowTimeOffset(offset int) { + nowOffset = offset +} diff --git a/xtime/duration.go b/xtime/duration.go new file mode 100644 index 0000000..084407e --- /dev/null +++ b/xtime/duration.go @@ -0,0 +1,83 @@ +package xtime + +import ( + "time" +) + +// 刷新点:时间边界 + +func GetDuration(t time.Time) (d time.Duration) { + return t.Sub(time.Now()) +} + +// 现在到明日刷新点的剩余时间间隔 +func GetDurationToNextHour() (d time.Duration) { + return GetDurationToNextNHour(1) +} + +// 现在到第N天刷新点的剩余时间间隔 (n为1是明天) +func GetDurationToNextNHour(n int) (d time.Duration) { + now := Now().StdTime() + next := Now().AddHours(n).StartOfHour().StdTime() + return next.Sub(now) +} + +// 现在到明日刷新点的剩余时间间隔 +func GetDurationToTomorrow() (d time.Duration) { + return GetDurationToNextNDay(1) +} + +// 现在到第N天刷新点的剩余时间间隔 (n为1是明天) +func GetDurationToNextNDay(n int) (d time.Duration) { + now := Now().StdTime() + next := Now().AddDays(n).StartOfDay().StdTime() + return next.Sub(now) +} + +// 现在到下周刷新点的剩余时间间隔 +func GetDurationToNextWeek() (expiration time.Duration) { + return GetDurationToNextNWeek(1) +} + +// 现在到下N周刷新点的剩余时间间隔 (n为1是下周) +func GetDurationToNextNWeek(n int) (expiration time.Duration) { + now := Now().StdTime() + nextN := Now().AddWeeks(n).StartOfWeek().StdTime() + return nextN.Sub(now) +} + +// 现在到下月刷新点的剩余时间间隔 +func GetDurationToNextMonth() (expiration time.Duration) { + return GetDurationToNextNMonth(1) +} + +// 现在到下N月刷新点的剩余时间间隔 (n为1是下月) +func GetDurationToNextNMonth(n int) (expiration time.Duration) { + now := Now().StdTime() + nextN := Now().AddMonths(n).StartOfMonth().StdTime() + return nextN.Sub(now) +} + +// 现在到下季度刷新点的剩余时间间隔 +func GetDurationToNextQuarter() (expiration time.Duration) { + return GetDurationToNextNQuarter(1) +} + +// 现在到下N季度刷新点的剩余时间间隔 (n为1是下年) +func GetDurationToNextNQuarter(n int) (expiration time.Duration) { + now := Now().StdTime() + nextN := Now().AddQuarters(n).StartOfQuarter().StdTime() + return nextN.Sub(now) +} + +// 现在到下年刷新点的剩余时间间隔 +func GetDurationToNextYear() (expiration time.Duration) { + return GetDurationToNextNYear(1) +} + +// 现在到下N年刷新点的剩余时间间隔 (n为1是下年) +func GetDurationToNextNYear(n int) (expiration time.Duration) { + now := Now().StdTime() + nextN := Now().AddYears(n).StartOfYear().StdTime() + return nextN.Sub(now) +} diff --git a/xtime/duration_test.go b/xtime/duration_test.go new file mode 100644 index 0000000..65b2896 --- /dev/null +++ b/xtime/duration_test.go @@ -0,0 +1,114 @@ +package xtime + +import ( + "fmt" + "testing" +) + +func TestGetDurationToNextHour(t *testing.T) { + now := Now() + target := Tomorrow() + d := GetDurationToNextHour() + fmt.Printf("now[%v] %s to next hour[%s] \n", + now, + d, + target, + ) +} + +func TestGetDurationToNextNHour(t *testing.T) { + n := 3 + now := Now() + target := Tomorrow() + d := GetDurationToNextNHour(n) + fmt.Printf("now[%v] %s to next %d hour[%s] \n", + now, + d, + n, + target, + ) +} + +func TestGetDurationToTomorrow(t *testing.T) { + now := Now() + target := Tomorrow() + d := GetDurationToTomorrow() + fmt.Printf("now[%v](%s) %s to tomorrow[%s](%s) \n", + FormatDate(now), + now, + d, + FormatDate(target), + target, + ) +} + +func TestGetDurationToNextNDay(t *testing.T) { + n := 3 + now := Now() + target := NextNDay(n) + d := GetDurationToNextNDay(n) + fmt.Printf("now[%v](%s) %s to next %v day[%s](%s) \n", + FormatDate(now), + now, + d, + n, + FormatDate(target), + target, + ) +} + +func TestGetDurationToNextWeek(t *testing.T) { + now := Now() + target := NextWeek() + d := GetDurationToNextWeek() + fmt.Printf("now[%v](%s) %s to next week[%s](%s) \n", + FormatWeek(now), + now, + d, + FormatWeek(target), + target, + ) +} + +func TestGetDurationToNextNWeek(t *testing.T) { + n := 3 + now := Now() + target := NextNWeek(n) + d := GetDurationToNextNWeek(n) + fmt.Printf("now[%v](%s) %s to next %v week[%s](%s) \n", + FormatWeek(now), + now, + d, + n, + FormatWeek(target), + target, + ) +} + +func TestGetDurationToNextMonth(t *testing.T) { + now := Now() + target := NextMonth() + d := GetDurationToNextMonth() + fmt.Printf("now[%v](%s) %s to next month[%s](%s) \n", + FormatMonth(now), + now, + d, + FormatMonth(target), + target, + ) +} + +func TestGetDurationToNextNMonth(t *testing.T) { + n := 3 + now := Now() + target := NextNMonth(n) + d := GetDurationToNextNMonth(n) + fmt.Printf("now[%v](%s) %s to next %v month[%s](%s) \n", + FormatMonth(now), + now, + d, + n, + FormatMonth(target), + target, + ) +} diff --git a/xtime/format.go b/xtime/format.go new file mode 100644 index 0000000..bd7a5af --- /dev/null +++ b/xtime/format.go @@ -0,0 +1,46 @@ +package xtime + +import "github.com/golang-module/carbon/v2" + +const ( + WeekLayout = "Y-W" + MonthLayout = "Y-m" + QuarterLayout = "Y-Q" + YearLayout = "Y" +) + +// 2006-01-02 + +func FormatDate(c carbon.Carbon) string { + return c.ToDateString() +} + +// 2006-55 (55周) + +func FormatWeek(c carbon.Carbon) string { + return c.Format(WeekLayout) +} + +// 2006-12 + +func FormatMonth(c carbon.Carbon) string { + return c.Format(MonthLayout) +} + +// 2006-04 (4季度) + +func FormatQuarter(c carbon.Carbon) string { + return c.Format(QuarterLayout) +} + +// 2006 + +func FormatYear(c carbon.Carbon) string { + return c.Format(YearLayout) +} + +// 20060102 + +func FormatDateNum(c carbon.Carbon) int { + return c.Year()*10000 + c.Month()*100 + c.Day() +} diff --git a/xtime/format_test.go b/xtime/format_test.go new file mode 100644 index 0000000..c9e008c --- /dev/null +++ b/xtime/format_test.go @@ -0,0 +1,32 @@ +package xtime + +import ( + "fmt" + "testing" +) + +func TestFormat(t *testing.T) { + fmt.Printf( + "FormatDate(Yesterday()): %v\nFormatDate(Today()): %v\nFormatDate(Tomorrow()): %v\nFormatDate(NextNDay(3)): %v\nFormatWeek(ThisWeek()): %v\nFormatWeek(LastWeek()): %v\nFormatWeek(NextWeek()): %v\nFormatWeek(NextNWeek(3)): %v\nFormatMonth(ThisMonth()): %v\nFormatMonth(LastMonth()): %v\nFormatMonth(NextMonth()): %v\nFormatMonth(NextNMonth(3)): %v\nFormatQuarter(ThisQuarter()): %v\nFormatQuarter(LastQuarter()): %v\nFormatQuarter(NextQuarter()): %v\nFormatQuarter(NextNQuarter(3)): %v\nFormatYear(ThisYear()): %v\nFormatYear(LastYear()): %v\nFormatYear(NextYear()): %v\nFormatYear(NextNYear(3)): %v\n", + FormatDate(Yesterday()), + FormatDate(Today()), + FormatDate(Tomorrow()), + FormatDate(NextNDay(3)), + FormatWeek(ThisWeek()), + FormatWeek(LastWeek()), + FormatWeek(NextWeek()), + FormatWeek(NextNWeek(3)), + FormatMonth(ThisMonth()), + FormatMonth(LastMonth()), + FormatMonth(NextMonth()), + FormatMonth(NextNMonth(3)), + FormatQuarter(ThisQuarter()), + FormatQuarter(LastQuarter()), + FormatQuarter(NextQuarter()), + FormatQuarter(NextNQuarter(3)), + FormatYear(ThisYear()), + FormatYear(LastYear()), + FormatYear(NextYear()), + FormatYear(NextNYear(3)), + ) +} diff --git a/xtime/os.go b/xtime/os.go new file mode 100644 index 0000000..c754374 --- /dev/null +++ b/xtime/os.go @@ -0,0 +1,11 @@ +package xtime + +import ( + "time" +) + +// 设置系统时区 + +func SetTimeOffset(offset int) { + time.Local = time.FixedZone("CST", offset) +} diff --git a/xtime/parser.go b/xtime/parser.go new file mode 100644 index 0000000..33ebd41 --- /dev/null +++ b/xtime/parser.go @@ -0,0 +1,41 @@ +package xtime + +import ( + "github.com/golang-module/carbon/v2" +) + +// 解析系统时区输出的字符串、时间戳,转为偏移时区 +// 注意如果你的时间戳和字符串已经是偏移时区输出的,就不应使用下列解析函数! + +func Parse(value string, offsets ...int) (c carbon.Carbon) { + c = carbon.Parse(value) + c = set(c, offsets...) + return c +} + +//goland:noinspection GoUnusedExportedFunction +func FromUnix(sec int64, offsets ...int) carbon.Carbon { + c := carbon.CreateFromTimestamp(sec) + c = set(c, offsets...) + return c +} + +func FromUnixMilli(msec int64, offsets ...int) carbon.Carbon { + c := carbon.CreateFromTimestampMilli(msec) + c = set(c, offsets...) + return c +} + +//goland:noinspection GoUnusedExportedFunction +func FromUnixMicro(msec int64, offsets ...int) carbon.Carbon { + c := carbon.CreateFromTimestampMicro(msec) + c = set(c, offsets...) + return c +} + +//goland:noinspection GoUnusedExportedFunction +func FromUnixNano(msec int64, offsets ...int) carbon.Carbon { + c := carbon.CreateFromTimestampNano(msec) + c = set(c, offsets...) + return c +} diff --git a/xtime/parser_test.go b/xtime/parser_test.go new file mode 100644 index 0000000..0c3da24 --- /dev/null +++ b/xtime/parser_test.go @@ -0,0 +1,16 @@ +package xtime + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + assert.Equal(t, Parse("2023-01-01 00:00:00").ToDateTimeString(), "2022-12-31 20:00:00") +} + +func TestFromMilli(t *testing.T) { + ts := int64(1672502400000) // "2023-01-01 00:00:00" + assert.Equal(t, FromUnixMilli(ts).ToDateTimeString(), "2022-12-31 20:00:00") +} diff --git a/xtime/timex.go b/xtime/timex.go new file mode 100644 index 0000000..137aabc --- /dev/null +++ b/xtime/timex.go @@ -0,0 +1,209 @@ +package xtime + +import ( + "fmt" + "time" + + "github.com/golang-module/carbon/v2" +) + +// 基于Carbon而不是Time +// 默认将 SaoPauloTimezoneOffset 作为时区偏移 +// 默认将 WeekStartsAt 作为每周起始 +// 注意新加的函数都要基于Now() + +// 设置时区 +// reverse:给Now()设置时传false,解析系统时区输出的字符串/时间戳时传true +func setLocation(c carbon.Carbon, offsets ...int) carbon.Carbon { + var loc *time.Location + if len(offsets) > 0 { + loc = time.FixedZone("CUS", offsets[len(offsets)-1]) + } else { + loc = time.FixedZone("CST", sysTimezoneOffset) + } + + return c.SetLocation(loc) +} + +func set(c carbon.Carbon, offsets ...int) carbon.Carbon { + // 设置周起始 + c = c.SetWeekStartsAt(WeekStartsAt) + // 设置时区 + c = setLocation(c, offsets...) + return c +} + +func Now(offsets ...int) (c carbon.Carbon) { + c = carbon.Now().AddSeconds(nowOffset) + c = set(c, offsets...) + return +} + +func ConvertToCarbon[T time.Time | carbon.Carbon | int64](t T, offsets ...int) carbon.Carbon { + var c carbon.Carbon + switch v := any(t).(type) { + case time.Time: + c = set(carbon.CreateFromStdTime(v), offsets...) + case carbon.Carbon: + c = set(v, offsets...) + case int64: + c = set(carbon.CreateFromTimestamp(v), offsets...) + } + return c +} + +// IsTodayTimestamp 判断该时间戳是否为今天 +func IsTodayTimestamp[T int | int64](timestamp T, offsets ...int) bool { + return IsToday(int64(timestamp), offsets...) +} + +// IsToday 判断时间是否为今天 +func IsToday[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool { + c := ConvertToCarbon(t, offsets...) + + return c.ToDateString() == Now().ToDateString() +} + +// IsBeforeNow 判断时间t是否在现在之前 +// example: +// +// now := time.Now() +// before := time.Now().Add(-time.Hour) +// IsBeforeNow(after) // true +func IsBeforeNow[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool { + return !IsAfterNow(t, offsets...) +} + +// IsAfterNow 判断时间t是否在现在之后 +// example: +// +// now := time.Now() +// after := time.Now().Add(time.Hour) +// IsAfterNow(after) // true +func IsAfterNow[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool { + switch v := any(t).(type) { + case time.Time: + return IsAfter(v, time.Now(), offsets...) + case carbon.Carbon: + return IsAfter(v, Now(), offsets...) + case int64: + return IsAfter(v, Now().Timestamp()) + default: + return false + } +} + +// IsBefore 判断时间t1是否在t2之前 +// example: +// t1 := time.Now().Add(-time.Hour) +// t2 := time.Now() +// IsBefore(t1,t2) // true +func IsBefore[T time.Time | carbon.Carbon | int64](t1, t2 T, offsets ...int) bool { + return !IsAfter(t1, t2, offsets...) +} + +// IsAfter 判断时间t1是否在t2之后 +// example: +// t1 := time.Now().Add(time.Hour) +// t2 := time.Now() +// IsAfter(t1,t2) // true +func IsAfter[T time.Time | carbon.Carbon | int64](t1, t2 T, offsets ...int) bool { + c1, c2 := ConvertToCarbon(t1, offsets...), ConvertToCarbon(t2, offsets...) + return c1.Gt(c2) +} + +// TimestampToTime 将时间戳转为 carbon.Carbon 类型 +func TimestampToTime[T int | int64](timestamp T, offsets ...int) carbon.Carbon { + return set(carbon.CreateFromTimestamp(int64(timestamp)), offsets...) +} + +// IsInDuration 判断某个时间在某个时间段内 +func IsInDuration[T time.Time | carbon.Carbon | int64](t, start, end T, offsets ...int) bool { + return IsAfter(t, start, offsets...) && IsBefore(t, end, offsets...) +} + +// FormatDuration 将起止时间,格式化为 format - format +func FormatDuration[T time.Time | carbon.Carbon | int64](start, end T, layout string, offsets ...int) string { + startC, endC := ConvertToCarbon(start, offsets...), ConvertToCarbon(end, offsets...) + return fmt.Sprintf("%s - %s", startC.StdTime().Format(layout), endC.StdTime().Format(layout)) +} + +func Yesterday(offsets ...int) carbon.Carbon { + return Now(offsets...).SubDay().StartOfDay() +} + +func Today(offsets ...int) carbon.Carbon { + return Now(offsets...).StartOfDay() +} + +func Tomorrow(offsets ...int) carbon.Carbon { + return Now(offsets...).AddDay().StartOfDay() +} + +func NextNDay(n int, offsets ...int) carbon.Carbon { + return Now(offsets...).AddDays(n).StartOfDay() +} + +func LastWeek(offsets ...int) carbon.Carbon { + return Now(offsets...).SubWeek().StartOfWeek() +} + +func ThisWeek(offsets ...int) carbon.Carbon { + return Now(offsets...).StartOfWeek() +} + +func NextWeek(offsets ...int) carbon.Carbon { + return Now(offsets...).AddWeek().StartOfWeek() +} + +func NextNWeek(n int, offsets ...int) carbon.Carbon { + return Now(offsets...).AddWeeks(n).StartOfWeek() +} + +func LastMonth(offsets ...int) carbon.Carbon { + return Now(offsets...).SubMonth().StartOfMonth() +} + +func ThisMonth(offsets ...int) carbon.Carbon { + return Now(offsets...).StartOfMonth() +} + +func NextMonth(offsets ...int) carbon.Carbon { + return Now(offsets...).AddMonth().StartOfMonth() +} + +func NextNMonth(n int, offsets ...int) carbon.Carbon { + return Now(offsets...).AddMonths(n).StartOfMonth() +} + +func LastQuarter(offsets ...int) carbon.Carbon { + return Now(offsets...).SubQuarter().StartOfQuarter() +} + +func ThisQuarter(offsets ...int) carbon.Carbon { + return Now(offsets...).StartOfQuarter() +} + +func NextQuarter(offsets ...int) carbon.Carbon { + return Now(offsets...).AddQuarter().StartOfQuarter() +} + +func NextNQuarter(n int, offsets ...int) carbon.Carbon { + return Now(offsets...).AddQuarters(n).StartOfQuarter() +} + +func LastYear(offsets ...int) carbon.Carbon { + return Now(offsets...).SubYear().StartOfYear() +} + +func ThisYear(offsets ...int) carbon.Carbon { + return Now(offsets...).StartOfYear() +} + +func NextYear(offsets ...int) carbon.Carbon { + return Now(offsets...).AddYear().StartOfYear() +} + +func NextNYear(n int, offsets ...int) carbon.Carbon { + return Now(offsets...).AddYears(n).StartOfYear() +} diff --git a/xtime/timex_test.go b/xtime/timex_test.go new file mode 100644 index 0000000..bdcf151 --- /dev/null +++ b/xtime/timex_test.go @@ -0,0 +1,146 @@ +package xtime + +import ( + "github.com/golang-module/carbon/v2" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestSetTimeOffset(t *testing.T) { + loc, err := time.LoadLocation("Asia/Shanghai") + assert.NoError(t, err) + loc2, err := time.LoadLocation("America/New_York") + assert.NoError(t, err) + a := time.Now().In(loc) + b := time.Now().In(loc2) + t.Log(a.String()) + t.Log(b.String()) + Now() + t.Log(a.Sub(b)) +} + +func TestFormatDayNum(t *testing.T) { + t1 := Parse("2023-01-01 00:00:00", 0) + assert.Equal(t, FormatDateNum(t1), 20221231) + t2 := Parse("2023-01-01 04:00:00", 0) + assert.Equal(t, FormatDateNum(t2), 20230101) +} + +func TestNow(t *testing.T) { + now := Now() + sc := carbon.Now(carbon.SaoPaulo) + + b := func(args ...int) []int { return args } + assert.Equal(t, b(now.Time()), b(sc.Time())) + +} + +func TestIsToday(t *testing.T) { + now := time.Now() + + assert.True(t, IsToday(now)) + + assert.False(t, IsToday(now.Add(24*time.Hour))) + assert.False(t, IsToday(now.Add(-24*time.Hour))) + +} + +func TestIsTodayTimestamp(t *testing.T) { + now := Now() + st := now.Timestamp() + + assert.True(t, IsTodayTimestamp(st)) + + assert.False(t, IsTodayTimestamp(now.Yesterday().Timestamp())) + assert.False(t, IsTodayTimestamp(now.AddDay().Timestamp())) +} + +func TestIsAfterNow(t *testing.T) { + t.Run("std Time", func(t *testing.T) { + now := time.Now() + after := now.Add(time.Hour) + assert.True(t, IsAfterNow(after)) + now = time.Now() + before := now.Add(-time.Hour) + assert.False(t, IsAfterNow(before)) + }) + + t.Run("carbon", func(t *testing.T) { + now := Now() + after := now.AddHour() + assert.True(t, IsAfterNow(after)) + now = Now() + before := now.SubHour() + assert.False(t, IsAfterNow(before)) + }) + t.Run("timestamp", func(t *testing.T) { + now := time.Now().Unix() + after := now + 3600 + assert.True(t, IsAfterNow(after)) + now = time.Now().Unix() + before := now - 3600 + assert.False(t, IsAfterNow(before)) + + }) +} + +func TestIsBeforeNow(t *testing.T) { + t.Run("std Time", func(t *testing.T) { + now := time.Now() + before := now.Add(-time.Hour) + assert.True(t, IsBeforeNow(before)) + now = time.Now() + after := now.Add(time.Hour) + assert.False(t, IsBeforeNow(after)) + }) + + t.Run("carbon", func(t *testing.T) { + now := Now() + before := now.SubHour() + assert.True(t, IsBeforeNow(before)) + now = Now() + after := now.AddHour() + assert.False(t, IsBeforeNow(after)) + }) + t.Run("timestamp", func(t *testing.T) { + now := time.Now().Unix() + before := now - 3600 + assert.True(t, IsBeforeNow(before)) + now = time.Now().Unix() + after := now + 3600 + assert.False(t, IsBeforeNow(after)) + + }) +} + +func TestIsInDuration(t *testing.T) { + + t.Run("std Time", func(t *testing.T) { + now := time.Now() + start := now.Add(-time.Hour) + end := now.Add(time.Hour) + assert.True(t, IsInDuration(now, start, end)) + assert.False(t, IsInDuration(now.Add(10*time.Hour), start, end)) + assert.False(t, IsInDuration(now.Add(-10*time.Hour), start, end)) + }) + + t.Run("carbon", func(t *testing.T) { + now := Now() + start := now.SubHour() + end := now.AddHour() + assert.True(t, IsInDuration(now, start, end)) + assert.False(t, IsInDuration(now.AddHours(10), start, end)) + assert.False(t, IsInDuration(now.SubHours(10), start, end)) + }) + t.Run("timestamp", func(t *testing.T) { + now := time.Now().Unix() + start := now - 3600 + end := now + 3600 + assert.True(t, IsInDuration(now, start, end)) + assert.False(t, IsInDuration(now+3600*10, start, end)) + assert.False(t, IsInDuration(now-3600*10, start, end)) + + }) + +}