Merge branch 'develop' into sdk
This commit is contained in:
commit
d2ec1b68a4
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,3 +23,5 @@ src/server/gen-go
|
||||
src/server/GeoLite2-Country
|
||||
src/server/test/GeoLite2-Country
|
||||
src/server/msg/Gameapi_grpc.pb.go
|
||||
src/server/unit_test/*.exe*
|
||||
src/server/unit_test/log*
|
||||
@ -3,6 +3,6 @@ package MergeConst
|
||||
const (
|
||||
Go_gc_percent = 200
|
||||
Go_gc_memory_limit = 1024 << 20
|
||||
Go_game_version = "1.0.3" // 游戏版本号,格式为 "主版本号.次版本号.修订号",每次发布新版本时需要更新
|
||||
Go_game_version = "1.0.4" // 游戏版本号,格式为 "主版本号.次版本号.修订号",每次发布新版本时需要更新
|
||||
Go_log_delete_days = 3 // 日志删除天数,超过这个天数的日志文件将被删除
|
||||
)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@ -6,7 +6,8 @@ import (
|
||||
"server/conf"
|
||||
"server/game"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@ -16,12 +17,22 @@ type backendServer struct {
|
||||
}
|
||||
|
||||
func (s *backendServer) ReloadActivity(ctx context.Context, req *msg.ReqActivityCfgReload) (*msg.ResActivityCfgReload, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("ReloadActivity panic: %v", r)
|
||||
}
|
||||
}()
|
||||
log.Debug("Received ReloadActivity request: %v", req)
|
||||
game.AcitivityCfgReload()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *backendServer) OrderShipping(ctx context.Context, req *msg.ReqOrderShipping) (*msg.ResOrderShipping, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("OrderShipping panic: %v", r)
|
||||
}
|
||||
}()
|
||||
log.Debug("Received OrderShipping request: %v", req)
|
||||
res, err := game.AdminShipping(req)
|
||||
if err != nil {
|
||||
@ -31,6 +42,21 @@ func (s *backendServer) OrderShipping(ctx context.Context, req *msg.ReqOrderShip
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *backendServer) UserDetail(ctx context.Context, req *msg.UserDetailParam) (*msg.ResUserDetail, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("UserDetail panic: %v", r)
|
||||
}
|
||||
}()
|
||||
log.Debug("Received UserDetail request: %v", req)
|
||||
res, err := game.AdminPlayerDetailInfo(req)
|
||||
if err != nil {
|
||||
log.Error("UserDetail error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func Start() {
|
||||
if conf.Server.RPCAddr == "" {
|
||||
log.Debug("RPC server address not configured, skipping gRPC server startup")
|
||||
|
||||
@ -2,8 +2,9 @@ package base
|
||||
|
||||
import (
|
||||
"server/conf"
|
||||
"server/pkg/github.com/name5566/leaf/chanrpc"
|
||||
"server/pkg/github.com/name5566/leaf/module"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/chanrpc"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/module"
|
||||
)
|
||||
|
||||
func NewSkeleton() *module.Skeleton {
|
||||
|
||||
@ -69,14 +69,14 @@ func BenchmarkChampionshipGroup(b *testing.B) {
|
||||
func BenchmarkPlayerInit(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
p := new(game.Player)
|
||||
p.InitPlayer("3625212")
|
||||
p.InitPlayer("aaa002")
|
||||
p.LoginBackData()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPlayerBackup(b *testing.B) {
|
||||
p := new(game.Player)
|
||||
p.InitPlayer("3625212")
|
||||
p.InitPlayer("aaa002")
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.BackUp()
|
||||
}
|
||||
@ -84,7 +84,7 @@ func BenchmarkPlayerBackup(b *testing.B) {
|
||||
|
||||
func BenchmarkPlayerRecover(b *testing.B) {
|
||||
p := new(game.Player)
|
||||
p.InitPlayer("3625212")
|
||||
p.InitPlayer("aaa002")
|
||||
backup := p.BackUp()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.Recover(backup)
|
||||
@ -93,7 +93,7 @@ func BenchmarkPlayerRecover(b *testing.B) {
|
||||
|
||||
func BenchmarkPlayerHandleMsg(b *testing.B) {
|
||||
p := new(game.Player)
|
||||
p.InitPlayer("3625212")
|
||||
p.InitPlayer("aaa002")
|
||||
list := make([]*msg.Msg, 0, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
m := &msg.Msg{
|
||||
|
||||
@ -6,9 +6,11 @@ import (
|
||||
"server/conf"
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"server/pkg/github.com/name5566/leaf/network"
|
||||
"sync"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/network"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -5,9 +5,11 @@ import (
|
||||
"server/conf"
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"server/pkg/github.com/name5566/leaf/network"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/network"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
func RevcServerMsg(m *msg.Msg) error {
|
||||
|
||||
@ -4,8 +4,9 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,8 +3,9 @@ package avatarCfg
|
||||
import (
|
||||
"math/rand"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,8 +3,9 @@ package baseCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,9 +4,10 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,7 +4,8 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,8 +4,9 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,9 +4,10 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,7 +4,8 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,9 +3,10 @@ package dailyTaskCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,9 +3,10 @@ package decorateCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,7 +3,8 @@ package emojiCfg
|
||||
import (
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,7 +3,8 @@ package endlessCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,8 +3,9 @@ package faceCfg
|
||||
import (
|
||||
"math/rand"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -2,7 +2,8 @@ package friendCfg
|
||||
|
||||
import (
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,8 +4,9 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,7 +3,8 @@ package guidecfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,8 +4,9 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,7 +3,8 @@ package handbookCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,7 +3,8 @@ package inviteCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,9 +4,10 @@ import (
|
||||
languageCfg "server/conf/language"
|
||||
"server/gamedata"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
var CFG_NAME = "Item"
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
var Server struct {
|
||||
|
||||
@ -5,9 +5,10 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -311,6 +312,15 @@ func GetUnlockLv() int {
|
||||
return gamedata.GetIntValue(data, "Value")
|
||||
}
|
||||
|
||||
func GetCatReturnGiftItems() []*item.Item {
|
||||
data, err := gamedata.GetDataByKey(CFG_LIMITED_TIME_EVENT_CONST, "Cat_Return_Gift_Items")
|
||||
if err != nil {
|
||||
log.Debug("GetCatReturnGiftItems err:%v", err)
|
||||
return nil
|
||||
}
|
||||
return gamedata.GetItemList(data, "Value")
|
||||
}
|
||||
|
||||
func GetCatSaleCD() int64 {
|
||||
data, err := gamedata.GetDataByKey(CFG_LIMITED_TIME_EVENT_CONST, "Event_Cooldown_7days")
|
||||
if err != nil {
|
||||
|
||||
@ -5,9 +5,10 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -4,8 +4,9 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -219,6 +220,14 @@ func GetInteractUnlock() int {
|
||||
return gamedata.GetIntValue(data, "Value")
|
||||
}
|
||||
|
||||
func GetRoomDailyTaskUnlock() int {
|
||||
data, err := gamedata.GetDataByKey(CFG_PLAYROOM_CONST, "RoomDailyTaskUnlock")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return gamedata.GetIntValue(data, "Value")
|
||||
}
|
||||
|
||||
func GetVisitorItem() int {
|
||||
data, err := gamedata.GetDataByKey(CFG_PLAYROOM_CONST, "VisitorItem")
|
||||
if err != nil {
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"DbName": "merge_pet_1",
|
||||
"HttpPort": ":8081",
|
||||
"AppPath": "./app",
|
||||
"TELOGDIR" : "./teLog/",
|
||||
"TELOGDIR" : "./log/teLog/",
|
||||
|
||||
"GameName": "pet_home_local",
|
||||
|
||||
|
||||
@ -4,8 +4,9 @@ import (
|
||||
"math"
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -3,7 +3,8 @@ package userCfg
|
||||
import (
|
||||
"server/game/mod/item"
|
||||
"server/gamedata"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
Copyright (C) 2012 Rob Figueiredo
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -1,125 +0,0 @@
|
||||
[](http://godoc.org/github.com/robfig/cron)
|
||||
[](https://travis-ci.org/robfig/cron)
|
||||
|
||||
# cron
|
||||
|
||||
Cron V3 has been released!
|
||||
|
||||
To download the specific tagged release, run:
|
||||
|
||||
go get github.com/robfig/cron/v3@v3.0.0
|
||||
|
||||
Import it in your program as:
|
||||
|
||||
import "github.com/robfig/cron/v3"
|
||||
|
||||
It requires Go 1.11 or later due to usage of Go Modules.
|
||||
|
||||
Refer to the documentation here:
|
||||
http://godoc.org/github.com/robfig/cron
|
||||
|
||||
The rest of this document describes the the advances in v3 and a list of
|
||||
breaking changes for users that wish to upgrade from an earlier version.
|
||||
|
||||
## Upgrading to v3 (June 2019)
|
||||
|
||||
cron v3 is a major upgrade to the library that addresses all outstanding bugs,
|
||||
feature requests, and rough edges. It is based on a merge of master which
|
||||
contains various fixes to issues found over the years and the v2 branch which
|
||||
contains some backwards-incompatible features like the ability to remove cron
|
||||
jobs. In addition, v3 adds support for Go Modules, cleans up rough edges like
|
||||
the timezone support, and fixes a number of bugs.
|
||||
|
||||
New features:
|
||||
|
||||
- Support for Go modules. Callers must now import this library as
|
||||
`github.com/robfig/cron/v3`, instead of `gopkg.in/...`
|
||||
|
||||
- Fixed bugs:
|
||||
- 0f01e6b parser: fix combining of Dow and Dom (#70)
|
||||
- dbf3220 adjust times when rolling the clock forward to handle non-existent midnight (#157)
|
||||
- eeecf15 spec_test.go: ensure an error is returned on 0 increment (#144)
|
||||
- 70971dc cron.Entries(): update request for snapshot to include a reply channel (#97)
|
||||
- 1cba5e6 cron: fix: removing a job causes the next scheduled job to run too late (#206)
|
||||
|
||||
- Standard cron spec parsing by default (first field is "minute"), with an easy
|
||||
way to opt into the seconds field (quartz-compatible). Although, note that the
|
||||
year field (optional in Quartz) is not supported.
|
||||
|
||||
- Extensible, key/value logging via an interface that complies with
|
||||
the https://github.com/go-logr/logr project.
|
||||
|
||||
- The new Chain & JobWrapper types allow you to install "interceptors" to add
|
||||
cross-cutting behavior like the following:
|
||||
- Recover any panics from jobs
|
||||
- Delay a job's execution if the previous run hasn't completed yet
|
||||
- Skip a job's execution if the previous run hasn't completed yet
|
||||
- Log each job's invocations
|
||||
- Notification when jobs are completed
|
||||
|
||||
It is backwards incompatible with both v1 and v2. These updates are required:
|
||||
|
||||
- The v1 branch accepted an optional seconds field at the beginning of the cron
|
||||
spec. This is non-standard and has led to a lot of confusion. The new default
|
||||
parser conforms to the standard as described by [the Cron wikipedia page].
|
||||
|
||||
UPDATING: To retain the old behavior, construct your Cron with a custom
|
||||
parser:
|
||||
|
||||
// Seconds field, required
|
||||
cron.New(cron.WithSeconds())
|
||||
|
||||
// Seconds field, optional
|
||||
cron.New(
|
||||
cron.WithParser(
|
||||
cron.SecondOptional | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor))
|
||||
|
||||
- The Cron type now accepts functional options on construction rather than the
|
||||
previous ad-hoc behavior modification mechanisms (setting a field, calling a setter).
|
||||
|
||||
UPDATING: Code that sets Cron.ErrorLogger or calls Cron.SetLocation must be
|
||||
updated to provide those values on construction.
|
||||
|
||||
- CRON_TZ is now the recommended way to specify the timezone of a single
|
||||
schedule, which is sanctioned by the specification. The legacy "TZ=" prefix
|
||||
will continue to be supported since it is unambiguous and easy to do so.
|
||||
|
||||
UPDATING: No update is required.
|
||||
|
||||
- By default, cron will no longer recover panics in jobs that it runs.
|
||||
Recovering can be surprising (see issue #192) and seems to be at odds with
|
||||
typical behavior of libraries. Relatedly, the `cron.WithPanicLogger` option
|
||||
has been removed to accommodate the more general JobWrapper type.
|
||||
|
||||
UPDATING: To opt into panic recovery and configure the panic logger:
|
||||
|
||||
cron.New(cron.WithChain(
|
||||
cron.Recover(logger), // or use cron.DefaultLogger
|
||||
))
|
||||
|
||||
- In adding support for https://github.com/go-logr/logr, `cron.WithVerboseLogger` was
|
||||
removed, since it is duplicative with the leveled logging.
|
||||
|
||||
UPDATING: Callers should use `WithLogger` and specify a logger that does not
|
||||
discard `Info` logs. For convenience, one is provided that wraps `*log.Logger`:
|
||||
|
||||
cron.New(
|
||||
cron.WithLogger(cron.VerbosePrintfLogger(logger)))
|
||||
|
||||
|
||||
### Background - Cron spec format
|
||||
|
||||
There are two cron spec formats in common usage:
|
||||
|
||||
- The "standard" cron format, described on [the Cron wikipedia page] and used by
|
||||
the cron Linux system utility.
|
||||
|
||||
- The cron format used by [the Quartz Scheduler], commonly used for scheduled
|
||||
jobs in Java software
|
||||
|
||||
[the Cron wikipedia page]: https://en.wikipedia.org/wiki/Cron
|
||||
[the Quartz Scheduler]: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
|
||||
|
||||
The original version of this package included an optional "seconds" field, which
|
||||
made it incompatible with both of these formats. Now, the "standard" format is
|
||||
the default format accepted, and the Quartz format is opt-in.
|
||||
@ -1,92 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JobWrapper decorates the given Job with some behavior.
|
||||
type JobWrapper func(Job) Job
|
||||
|
||||
// Chain is a sequence of JobWrappers that decorates submitted jobs with
|
||||
// cross-cutting behaviors like logging or synchronization.
|
||||
type Chain struct {
|
||||
wrappers []JobWrapper
|
||||
}
|
||||
|
||||
// NewChain returns a Chain consisting of the given JobWrappers.
|
||||
func NewChain(c ...JobWrapper) Chain {
|
||||
return Chain{c}
|
||||
}
|
||||
|
||||
// Then decorates the given job with all JobWrappers in the chain.
|
||||
//
|
||||
// This:
|
||||
// NewChain(m1, m2, m3).Then(job)
|
||||
// is equivalent to:
|
||||
// m1(m2(m3(job)))
|
||||
func (c Chain) Then(j Job) Job {
|
||||
for i := range c.wrappers {
|
||||
j = c.wrappers[len(c.wrappers)-i-1](j)
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
// Recover panics in wrapped jobs and log them with the provided logger.
|
||||
func Recover(logger Logger) JobWrapper {
|
||||
return func(j Job) Job {
|
||||
return FuncJob(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
const size = 64 << 10
|
||||
buf := make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
logger.Error(err, "panic", "stack", "...\n"+string(buf))
|
||||
}
|
||||
}()
|
||||
j.Run()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DelayIfStillRunning serializes jobs, delaying subsequent runs until the
|
||||
// previous one is complete. Jobs running after a delay of more than a minute
|
||||
// have the delay logged at Info.
|
||||
func DelayIfStillRunning(logger Logger) JobWrapper {
|
||||
return func(j Job) Job {
|
||||
var mu sync.Mutex
|
||||
return FuncJob(func() {
|
||||
start := time.Now()
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if dur := time.Since(start); dur > time.Minute {
|
||||
logger.Info("delay", "duration", dur)
|
||||
}
|
||||
j.Run()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SkipIfStillRunning skips an invocation of the Job if a previous invocation is
|
||||
// still running. It logs skips to the given logger at Info level.
|
||||
func SkipIfStillRunning(logger Logger) JobWrapper {
|
||||
var ch = make(chan struct{}, 1)
|
||||
ch <- struct{}{}
|
||||
return func(j Job) Job {
|
||||
return FuncJob(func() {
|
||||
select {
|
||||
case v := <-ch:
|
||||
j.Run()
|
||||
ch <- v
|
||||
default:
|
||||
logger.Info("skip")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,221 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func appendingJob(slice *[]int, value int) Job {
|
||||
var m sync.Mutex
|
||||
return FuncJob(func() {
|
||||
m.Lock()
|
||||
*slice = append(*slice, value)
|
||||
m.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func appendingWrapper(slice *[]int, value int) JobWrapper {
|
||||
return func(j Job) Job {
|
||||
return FuncJob(func() {
|
||||
appendingJob(slice, value).Run()
|
||||
j.Run()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
var nums []int
|
||||
var (
|
||||
append1 = appendingWrapper(&nums, 1)
|
||||
append2 = appendingWrapper(&nums, 2)
|
||||
append3 = appendingWrapper(&nums, 3)
|
||||
append4 = appendingJob(&nums, 4)
|
||||
)
|
||||
NewChain(append1, append2, append3).Then(append4).Run()
|
||||
if !reflect.DeepEqual(nums, []int{1, 2, 3, 4}) {
|
||||
t.Error("unexpected order of calls:", nums)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChainRecover(t *testing.T) {
|
||||
panickingJob := FuncJob(func() {
|
||||
panic("panickingJob panics")
|
||||
})
|
||||
|
||||
t.Run("panic exits job by default", func(t *testing.T) {
|
||||
defer func() {
|
||||
if err := recover(); err == nil {
|
||||
t.Errorf("panic expected, but none received")
|
||||
}
|
||||
}()
|
||||
NewChain().Then(panickingJob).
|
||||
Run()
|
||||
})
|
||||
|
||||
t.Run("Recovering JobWrapper recovers", func(t *testing.T) {
|
||||
NewChain(Recover(PrintfLogger(log.New(ioutil.Discard, "", 0)))).
|
||||
Then(panickingJob).
|
||||
Run()
|
||||
})
|
||||
|
||||
t.Run("composed with the *IfStillRunning wrappers", func(t *testing.T) {
|
||||
NewChain(Recover(PrintfLogger(log.New(ioutil.Discard, "", 0)))).
|
||||
Then(panickingJob).
|
||||
Run()
|
||||
})
|
||||
}
|
||||
|
||||
type countJob struct {
|
||||
m sync.Mutex
|
||||
started int
|
||||
done int
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (j *countJob) Run() {
|
||||
j.m.Lock()
|
||||
j.started++
|
||||
j.m.Unlock()
|
||||
time.Sleep(j.delay)
|
||||
j.m.Lock()
|
||||
j.done++
|
||||
j.m.Unlock()
|
||||
}
|
||||
|
||||
func (j *countJob) Started() int {
|
||||
defer j.m.Unlock()
|
||||
j.m.Lock()
|
||||
return j.started
|
||||
}
|
||||
|
||||
func (j *countJob) Done() int {
|
||||
defer j.m.Unlock()
|
||||
j.m.Lock()
|
||||
return j.done
|
||||
}
|
||||
|
||||
func TestChainDelayIfStillRunning(t *testing.T) {
|
||||
|
||||
t.Run("runs immediately", func(t *testing.T) {
|
||||
var j countJob
|
||||
wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)
|
||||
go wrappedJob.Run()
|
||||
time.Sleep(2 * time.Millisecond) // Give the job 2ms to complete.
|
||||
if c := j.Done(); c != 1 {
|
||||
t.Errorf("expected job run once, immediately, got %d", c)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second run immediate if first done", func(t *testing.T) {
|
||||
var j countJob
|
||||
wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)
|
||||
go func() {
|
||||
go wrappedJob.Run()
|
||||
time.Sleep(time.Millisecond)
|
||||
go wrappedJob.Run()
|
||||
}()
|
||||
time.Sleep(3 * time.Millisecond) // Give both jobs 3ms to complete.
|
||||
if c := j.Done(); c != 2 {
|
||||
t.Errorf("expected job run twice, immediately, got %d", c)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second run delayed if first not done", func(t *testing.T) {
|
||||
var j countJob
|
||||
j.delay = 10 * time.Millisecond
|
||||
wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)
|
||||
go func() {
|
||||
go wrappedJob.Run()
|
||||
time.Sleep(time.Millisecond)
|
||||
go wrappedJob.Run()
|
||||
}()
|
||||
|
||||
// After 5ms, the first job is still in progress, and the second job was
|
||||
// run but should be waiting for it to finish.
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
started, done := j.Started(), j.Done()
|
||||
if started != 1 || done != 0 {
|
||||
t.Error("expected first job started, but not finished, got", started, done)
|
||||
}
|
||||
|
||||
// Verify that the second job completes.
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
started, done = j.Started(), j.Done()
|
||||
if started != 2 || done != 2 {
|
||||
t.Error("expected both jobs done, got", started, done)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestChainSkipIfStillRunning(t *testing.T) {
|
||||
|
||||
t.Run("runs immediately", func(t *testing.T) {
|
||||
var j countJob
|
||||
wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)
|
||||
go wrappedJob.Run()
|
||||
time.Sleep(2 * time.Millisecond) // Give the job 2ms to complete.
|
||||
if c := j.Done(); c != 1 {
|
||||
t.Errorf("expected job run once, immediately, got %d", c)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second run immediate if first done", func(t *testing.T) {
|
||||
var j countJob
|
||||
wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)
|
||||
go func() {
|
||||
go wrappedJob.Run()
|
||||
time.Sleep(time.Millisecond)
|
||||
go wrappedJob.Run()
|
||||
}()
|
||||
time.Sleep(3 * time.Millisecond) // Give both jobs 3ms to complete.
|
||||
if c := j.Done(); c != 2 {
|
||||
t.Errorf("expected job run twice, immediately, got %d", c)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second run skipped if first not done", func(t *testing.T) {
|
||||
var j countJob
|
||||
j.delay = 10 * time.Millisecond
|
||||
wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)
|
||||
go func() {
|
||||
go wrappedJob.Run()
|
||||
time.Sleep(time.Millisecond)
|
||||
go wrappedJob.Run()
|
||||
}()
|
||||
|
||||
// After 5ms, the first job is still in progress, and the second job was
|
||||
// aleady skipped.
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
started, done := j.Started(), j.Done()
|
||||
if started != 1 || done != 0 {
|
||||
t.Error("expected first job started, but not finished, got", started, done)
|
||||
}
|
||||
|
||||
// Verify that the first job completes and second does not run.
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
started, done = j.Started(), j.Done()
|
||||
if started != 1 || done != 1 {
|
||||
t.Error("expected second job skipped, got", started, done)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("skip 10 jobs on rapid fire", func(t *testing.T) {
|
||||
var j countJob
|
||||
j.delay = 10 * time.Millisecond
|
||||
wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)
|
||||
for i := 0; i < 11; i++ {
|
||||
go wrappedJob.Run()
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
done := j.Done()
|
||||
if done != 1 {
|
||||
t.Error("expected 1 jobs executed, 10 jobs dropped, got", done)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
|
||||
// It does not support jobs more frequent than once a second.
|
||||
type ConstantDelaySchedule struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// Every returns a crontab Schedule that activates once every duration.
|
||||
// Delays of less than a second are not supported (will round up to 1 second).
|
||||
// Any fields less than a Second are truncated.
|
||||
func Every(duration time.Duration) ConstantDelaySchedule {
|
||||
if duration < time.Second {
|
||||
duration = time.Second
|
||||
}
|
||||
return ConstantDelaySchedule{
|
||||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
|
||||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConstantDelayNext(t *testing.T) {
|
||||
tests := []struct {
|
||||
time string
|
||||
delay time.Duration
|
||||
expected string
|
||||
}{
|
||||
// Simple cases
|
||||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
|
||||
{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"},
|
||||
{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"},
|
||||
|
||||
// Wrap around hours
|
||||
{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"},
|
||||
|
||||
// Wrap around days
|
||||
{"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"},
|
||||
{"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"},
|
||||
|
||||
// Wrap around months
|
||||
{"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"},
|
||||
|
||||
// Wrap around minute, hour, day, month, and year
|
||||
{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"},
|
||||
|
||||
// Round to nearest second on the delay
|
||||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
|
||||
|
||||
// Round up to 1 second if the duration is less.
|
||||
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"},
|
||||
|
||||
// Round to nearest second when calculating the next time.
|
||||
{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"},
|
||||
|
||||
// Round to nearest second for both.
|
||||
{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
|
||||
}
|
||||
|
||||
for _, c := range tests {
|
||||
actual := Every(c.delay).Next(getTime(c.time))
|
||||
expected := getTime(c.expected)
|
||||
if actual != expected {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,350 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron keeps track of any number of entries, invoking the associated func as
|
||||
// specified by the schedule. It may be started, stopped, and the entries may
|
||||
// be inspected while running.
|
||||
type Cron struct {
|
||||
entries []*Entry
|
||||
chain Chain
|
||||
stop chan struct{}
|
||||
add chan *Entry
|
||||
remove chan EntryID
|
||||
snapshot chan chan []Entry
|
||||
running bool
|
||||
logger Logger
|
||||
runningMu sync.Mutex
|
||||
location *time.Location
|
||||
parser Parser
|
||||
nextID EntryID
|
||||
jobWaiter sync.WaitGroup
|
||||
}
|
||||
|
||||
// Job is an interface for submitted cron jobs.
|
||||
type Job interface {
|
||||
Run()
|
||||
}
|
||||
|
||||
// Schedule describes a job's duty cycle.
|
||||
type Schedule interface {
|
||||
// Next returns the next activation time, later than the given time.
|
||||
// Next is invoked initially, and then each time the job is run.
|
||||
Next(time.Time) time.Time
|
||||
}
|
||||
|
||||
// EntryID identifies an entry within a Cron instance
|
||||
type EntryID int
|
||||
|
||||
// Entry consists of a schedule and the func to execute on that schedule.
|
||||
type Entry struct {
|
||||
// ID is the cron-assigned ID of this entry, which may be used to look up a
|
||||
// snapshot or remove it.
|
||||
ID EntryID
|
||||
|
||||
// Schedule on which this job should be run.
|
||||
Schedule Schedule
|
||||
|
||||
// Next time the job will run, or the zero time if Cron has not been
|
||||
// started or this entry's schedule is unsatisfiable
|
||||
Next time.Time
|
||||
|
||||
// Prev is the last time this job was run, or the zero time if never.
|
||||
Prev time.Time
|
||||
|
||||
// WrappedJob is the thing to run when the Schedule is activated.
|
||||
WrappedJob Job
|
||||
|
||||
// Job is the thing that was submitted to cron.
|
||||
// It is kept around so that user code that needs to get at the job later,
|
||||
// e.g. via Entries() can do so.
|
||||
Job Job
|
||||
}
|
||||
|
||||
// Valid returns true if this is not the zero entry.
|
||||
func (e Entry) Valid() bool { return e.ID != 0 }
|
||||
|
||||
// byTime is a wrapper for sorting the entry array by time
|
||||
// (with zero time at the end).
|
||||
type byTime []*Entry
|
||||
|
||||
func (s byTime) Len() int { return len(s) }
|
||||
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byTime) Less(i, j int) bool {
|
||||
// Two zero times should return false.
|
||||
// Otherwise, zero is "greater" than any other time.
|
||||
// (To sort it at the end of the list.)
|
||||
if s[i].Next.IsZero() {
|
||||
return false
|
||||
}
|
||||
if s[j].Next.IsZero() {
|
||||
return true
|
||||
}
|
||||
return s[i].Next.Before(s[j].Next)
|
||||
}
|
||||
|
||||
// New returns a new Cron job runner, modified by the given options.
|
||||
//
|
||||
// Available Settings
|
||||
//
|
||||
// Time Zone
|
||||
// Description: The time zone in which schedules are interpreted
|
||||
// Default: time.Local
|
||||
//
|
||||
// Parser
|
||||
// Description: Parser converts cron spec strings into cron.Schedules.
|
||||
// Default: Accepts this spec: https://en.wikipedia.org/wiki/Cron
|
||||
//
|
||||
// Chain
|
||||
// Description: Wrap submitted jobs to customize behavior.
|
||||
// Default: A chain that recovers panics and logs them to stderr.
|
||||
//
|
||||
// See "cron.With*" to modify the default behavior.
|
||||
func New(opts ...Option) *Cron {
|
||||
c := &Cron{
|
||||
entries: nil,
|
||||
chain: NewChain(),
|
||||
add: make(chan *Entry),
|
||||
stop: make(chan struct{}),
|
||||
snapshot: make(chan chan []Entry),
|
||||
remove: make(chan EntryID),
|
||||
running: false,
|
||||
runningMu: sync.Mutex{},
|
||||
logger: DefaultLogger,
|
||||
location: time.Local,
|
||||
parser: standardParser,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// FuncJob is a wrapper that turns a func() into a cron.Job
|
||||
type FuncJob func()
|
||||
|
||||
func (f FuncJob) Run() { f() }
|
||||
|
||||
// AddFunc adds a func to the Cron to be run on the given schedule.
|
||||
// The spec is parsed using the time zone of this Cron instance as the default.
|
||||
// An opaque ID is returned that can be used to later remove it.
|
||||
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
|
||||
return c.AddJob(spec, FuncJob(cmd))
|
||||
}
|
||||
|
||||
// AddJob adds a Job to the Cron to be run on the given schedule.
|
||||
// The spec is parsed using the time zone of this Cron instance as the default.
|
||||
// An opaque ID is returned that can be used to later remove it.
|
||||
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) {
|
||||
schedule, err := c.parser.Parse(spec)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.Schedule(schedule, cmd), nil
|
||||
}
|
||||
|
||||
// Schedule adds a Job to the Cron to be run on the given schedule.
|
||||
// The job is wrapped with the configured Chain.
|
||||
func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
c.nextID++
|
||||
entry := &Entry{
|
||||
ID: c.nextID,
|
||||
Schedule: schedule,
|
||||
WrappedJob: c.chain.Then(cmd),
|
||||
Job: cmd,
|
||||
}
|
||||
if !c.running {
|
||||
c.entries = append(c.entries, entry)
|
||||
} else {
|
||||
c.add <- entry
|
||||
}
|
||||
return entry.ID
|
||||
}
|
||||
|
||||
// Entries returns a snapshot of the cron entries.
|
||||
func (c *Cron) Entries() []Entry {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
replyChan := make(chan []Entry, 1)
|
||||
c.snapshot <- replyChan
|
||||
return <-replyChan
|
||||
}
|
||||
return c.entrySnapshot()
|
||||
}
|
||||
|
||||
// Location gets the time zone location
|
||||
func (c *Cron) Location() *time.Location {
|
||||
return c.location
|
||||
}
|
||||
|
||||
// Entry returns a snapshot of the given entry, or nil if it couldn't be found.
|
||||
func (c *Cron) Entry(id EntryID) Entry {
|
||||
for _, entry := range c.Entries() {
|
||||
if id == entry.ID {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return Entry{}
|
||||
}
|
||||
|
||||
// Remove an entry from being run in the future.
|
||||
func (c *Cron) Remove(id EntryID) {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
c.remove <- id
|
||||
} else {
|
||||
c.removeEntry(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the cron scheduler in its own goroutine, or no-op if already started.
|
||||
func (c *Cron) Start() {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
go c.run()
|
||||
}
|
||||
|
||||
// Run the cron scheduler, or no-op if already running.
|
||||
func (c *Cron) Run() {
|
||||
c.runningMu.Lock()
|
||||
if c.running {
|
||||
c.runningMu.Unlock()
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
c.runningMu.Unlock()
|
||||
c.run()
|
||||
}
|
||||
|
||||
// run the scheduler.. this is private just due to the need to synchronize
|
||||
// access to the 'running' state variable.
|
||||
func (c *Cron) run() {
|
||||
c.logger.Info("start")
|
||||
|
||||
// Figure out the next activation times for each entry.
|
||||
now := c.now()
|
||||
for _, entry := range c.entries {
|
||||
entry.Next = entry.Schedule.Next(now)
|
||||
c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next)
|
||||
}
|
||||
|
||||
for {
|
||||
// Determine the next entry to run.
|
||||
sort.Sort(byTime(c.entries))
|
||||
|
||||
var timer *time.Timer
|
||||
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
|
||||
// If there are no entries yet, just sleep - it still handles new entries
|
||||
// and stop requests.
|
||||
timer = time.NewTimer(100000 * time.Hour)
|
||||
} else {
|
||||
timer = time.NewTimer(c.entries[0].Next.Sub(now))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case now = <-timer.C:
|
||||
now = now.In(c.location)
|
||||
c.logger.Info("wake", "now", now)
|
||||
|
||||
// Run every entry whose next time was less than now
|
||||
for _, e := range c.entries {
|
||||
if e.Next.After(now) || e.Next.IsZero() {
|
||||
break
|
||||
}
|
||||
c.startJob(e.WrappedJob)
|
||||
e.Prev = e.Next
|
||||
e.Next = e.Schedule.Next(now)
|
||||
c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)
|
||||
}
|
||||
|
||||
case newEntry := <-c.add:
|
||||
timer.Stop()
|
||||
now = c.now()
|
||||
newEntry.Next = newEntry.Schedule.Next(now)
|
||||
c.entries = append(c.entries, newEntry)
|
||||
c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next)
|
||||
|
||||
case replyChan := <-c.snapshot:
|
||||
replyChan <- c.entrySnapshot()
|
||||
continue
|
||||
|
||||
case <-c.stop:
|
||||
timer.Stop()
|
||||
c.logger.Info("stop")
|
||||
return
|
||||
|
||||
case id := <-c.remove:
|
||||
timer.Stop()
|
||||
now = c.now()
|
||||
c.removeEntry(id)
|
||||
c.logger.Info("removed", "entry", id)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startJob runs the given job in a new goroutine.
|
||||
func (c *Cron) startJob(j Job) {
|
||||
c.jobWaiter.Add(1)
|
||||
go func() {
|
||||
defer c.jobWaiter.Done()
|
||||
j.Run()
|
||||
}()
|
||||
}
|
||||
|
||||
// now returns current time in c location
|
||||
func (c *Cron) now() time.Time {
|
||||
return time.Now().In(c.location)
|
||||
}
|
||||
|
||||
// Stop stops the cron scheduler if it is running; otherwise it does nothing.
|
||||
// A context is returned so the caller can wait for running jobs to complete.
|
||||
func (c *Cron) Stop() context.Context {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
c.stop <- struct{}{}
|
||||
c.running = false
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
c.jobWaiter.Wait()
|
||||
cancel()
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// entrySnapshot returns a copy of the current cron entry list.
|
||||
func (c *Cron) entrySnapshot() []Entry {
|
||||
var entries = make([]Entry, len(c.entries))
|
||||
for i, e := range c.entries {
|
||||
entries[i] = *e
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (c *Cron) removeEntry(id EntryID) {
|
||||
var entries []*Entry
|
||||
for _, e := range c.entries {
|
||||
if e.ID != id {
|
||||
entries = append(entries, e)
|
||||
}
|
||||
}
|
||||
c.entries = entries
|
||||
}
|
||||
@ -1,702 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Many tests schedule a job for every second, and then wait at most a second
|
||||
// for it to run. This amount is just slightly larger than 1 second to
|
||||
// compensate for a few milliseconds of runtime.
|
||||
const OneSecond = 1*time.Second + 50*time.Millisecond
|
||||
|
||||
type syncWriter struct {
|
||||
wr bytes.Buffer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (sw *syncWriter) Write(data []byte) (n int, err error) {
|
||||
sw.m.Lock()
|
||||
n, err = sw.wr.Write(data)
|
||||
sw.m.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (sw *syncWriter) String() string {
|
||||
sw.m.Lock()
|
||||
defer sw.m.Unlock()
|
||||
return sw.wr.String()
|
||||
}
|
||||
|
||||
func newBufLogger(sw *syncWriter) Logger {
|
||||
return PrintfLogger(log.New(sw, "", log.LstdFlags))
|
||||
}
|
||||
|
||||
func TestFuncPanicRecovery(t *testing.T) {
|
||||
var buf syncWriter
|
||||
cron := New(WithParser(secondParser),
|
||||
WithChain(Recover(newBufLogger(&buf))))
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
cron.AddFunc("* * * * * ?", func() {
|
||||
panic("YOLO")
|
||||
})
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
if !strings.Contains(buf.String(), "YOLO") {
|
||||
t.Error("expected a panic to be logged, got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type DummyJob struct{}
|
||||
|
||||
func (d DummyJob) Run() {
|
||||
panic("YOLO")
|
||||
}
|
||||
|
||||
func TestJobPanicRecovery(t *testing.T) {
|
||||
var job DummyJob
|
||||
|
||||
var buf syncWriter
|
||||
cron := New(WithParser(secondParser),
|
||||
WithChain(Recover(newBufLogger(&buf))))
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
cron.AddJob("* * * * * ?", job)
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
if !strings.Contains(buf.String(), "YOLO") {
|
||||
t.Error("expected a panic to be logged, got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Start and stop cron with no entries.
|
||||
func TestNoEntries(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.Fatal("expected cron will be stopped immediately")
|
||||
case <-stop(cron):
|
||||
}
|
||||
}
|
||||
|
||||
// Start, stop, then add an entry. Verify entry doesn't run.
|
||||
func TestStopCausesJobsToNotRun(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
cron.Stop()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
// No job ran!
|
||||
case <-wait(wg):
|
||||
t.Fatal("expected stopped cron does not run any job")
|
||||
}
|
||||
}
|
||||
|
||||
// Add a job, start cron, expect it runs.
|
||||
func TestAddBeforeRunning(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
// Give cron 2 seconds to run our job (which is always activated).
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.Fatal("expected job runs")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Start cron, add a job, expect it runs.
|
||||
func TestAddWhileRunning(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.Fatal("expected job runs")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test for #34. Adding a job after calling start results in multiple job invocations
|
||||
func TestAddWhileRunningWithDelay(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
time.Sleep(5 * time.Second)
|
||||
var calls int64
|
||||
cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
|
||||
|
||||
<-time.After(OneSecond)
|
||||
if atomic.LoadInt64(&calls) != 1 {
|
||||
t.Errorf("called %d times, expected 1\n", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// Add a job, remove a job, start cron, expect nothing runs.
|
||||
func TestRemoveBeforeRunning(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.Remove(id)
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
// Success, shouldn't run
|
||||
case <-wait(wg):
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// Start cron, add a job, remove it, expect it doesn't run.
|
||||
func TestRemoveWhileRunning(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.Remove(id)
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
case <-wait(wg):
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// Test timing with Entries.
|
||||
func TestSnapshotEntries(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc("@every 2s", func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
// Cron should fire in 2 seconds. After 1 second, call Entries.
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
cron.Entries()
|
||||
}
|
||||
|
||||
// Even though Entries was called, the cron should fire at the 2 second mark.
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.Error("expected job runs at 2 second mark")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the entries are correctly sorted.
|
||||
// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
|
||||
// that the immediate entry runs immediately.
|
||||
// Also: Test that multiple jobs run in the same instant.
|
||||
func TestMultipleEntries(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("0 0 0 1 1 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
id1, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
|
||||
id2, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
|
||||
cron.AddFunc("0 0 0 31 12 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
cron.Remove(id1)
|
||||
cron.Start()
|
||||
cron.Remove(id2)
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.Error("expected job run in proper order")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test running the same job twice.
|
||||
func TestRunningJobTwice(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("0 0 0 1 1 ?", func() {})
|
||||
cron.AddFunc("0 0 0 31 12 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(2 * OneSecond):
|
||||
t.Error("expected job fires 2 times")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunningMultipleSchedules(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("0 0 0 1 1 ?", func() {})
|
||||
cron.AddFunc("0 0 0 31 12 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.Schedule(Every(time.Minute), FuncJob(func() {}))
|
||||
cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
|
||||
cron.Schedule(Every(time.Hour), FuncJob(func() {}))
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(2 * OneSecond):
|
||||
t.Error("expected job fires 2 times")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the cron is run in the local time zone (as opposed to UTC).
|
||||
func TestLocalTimezone(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
now := time.Now()
|
||||
// FIX: Issue #205
|
||||
// This calculation doesn't work in seconds 58 or 59.
|
||||
// Take the easy way out and sleep.
|
||||
if now.Second() >= 58 {
|
||||
time.Sleep(2 * time.Second)
|
||||
now = time.Now()
|
||||
}
|
||||
spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
|
||||
now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc(spec, func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond * 2):
|
||||
t.Error("expected job fires 2 times")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the cron is run in the given time zone (as opposed to local).
|
||||
func TestNonLocalTimezone(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
loc, err := time.LoadLocation("Atlantic/Cape_Verde")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to load time zone Atlantic/Cape_Verde: %+v", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
now := time.Now().In(loc)
|
||||
// FIX: Issue #205
|
||||
// This calculation doesn't work in seconds 58 or 59.
|
||||
// Take the easy way out and sleep.
|
||||
if now.Second() >= 58 {
|
||||
time.Sleep(2 * time.Second)
|
||||
now = time.Now().In(loc)
|
||||
}
|
||||
spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
|
||||
now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
|
||||
|
||||
cron := New(WithLocation(loc), WithParser(secondParser))
|
||||
cron.AddFunc(spec, func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond * 2):
|
||||
t.Error("expected job fires 2 times")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test that calling stop before start silently returns without
|
||||
// blocking the stop channel.
|
||||
func TestStopWithoutStart(t *testing.T) {
|
||||
cron := New()
|
||||
cron.Stop()
|
||||
}
|
||||
|
||||
type testJob struct {
|
||||
wg *sync.WaitGroup
|
||||
name string
|
||||
}
|
||||
|
||||
func (t testJob) Run() {
|
||||
t.wg.Done()
|
||||
}
|
||||
|
||||
// Test that adding an invalid job spec returns an error
|
||||
func TestInvalidJobSpec(t *testing.T) {
|
||||
cron := New()
|
||||
_, err := cron.AddJob("this will not parse", nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected an error with invalid spec, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test blocking run method behaves as Start()
|
||||
func TestBlockingRun(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
var unblockChan = make(chan struct{})
|
||||
|
||||
go func() {
|
||||
cron.Run()
|
||||
close(unblockChan)
|
||||
}()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.Error("expected job fires")
|
||||
case <-unblockChan:
|
||||
t.Error("expected that Run() blocks")
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test that double-running is a no-op
|
||||
func TestStartNoop(t *testing.T) {
|
||||
var tickChan = make(chan struct{}, 2)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("* * * * * ?", func() {
|
||||
tickChan <- struct{}{}
|
||||
})
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
// Wait for the first firing to ensure the runner is going
|
||||
<-tickChan
|
||||
|
||||
cron.Start()
|
||||
|
||||
<-tickChan
|
||||
|
||||
// Fail if this job fires again in a short period, indicating a double-run
|
||||
select {
|
||||
case <-time.After(time.Millisecond):
|
||||
case <-tickChan:
|
||||
t.Error("expected job fires exactly twice")
|
||||
}
|
||||
}
|
||||
|
||||
// Simple test using Runnables.
|
||||
func TestJob(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := newWithSeconds()
|
||||
cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"})
|
||||
cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
|
||||
job2, _ := cron.AddJob("* * * * * ?", testJob{wg, "job2"})
|
||||
cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
|
||||
cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
|
||||
job5 := cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
|
||||
|
||||
// Test getting an Entry pre-Start.
|
||||
if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
|
||||
t.Error("wrong job retrieved:", actualName)
|
||||
}
|
||||
if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
|
||||
t.Error("wrong job retrieved:", actualName)
|
||||
}
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(OneSecond):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
|
||||
// Ensure the entries are in the right order.
|
||||
expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
|
||||
|
||||
var actuals []string
|
||||
for _, entry := range cron.Entries() {
|
||||
actuals = append(actuals, entry.Job.(testJob).name)
|
||||
}
|
||||
|
||||
for i, expected := range expecteds {
|
||||
if actuals[i] != expected {
|
||||
t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
|
||||
}
|
||||
}
|
||||
|
||||
// Test getting Entries.
|
||||
if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
|
||||
t.Error("wrong job retrieved:", actualName)
|
||||
}
|
||||
if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
|
||||
t.Error("wrong job retrieved:", actualName)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #206
|
||||
// Ensure that the next run of a job after removing an entry is accurate.
|
||||
func TestScheduleAfterRemoval(t *testing.T) {
|
||||
var wg1 sync.WaitGroup
|
||||
var wg2 sync.WaitGroup
|
||||
wg1.Add(1)
|
||||
wg2.Add(1)
|
||||
|
||||
// The first time this job is run, set a timer and remove the other job
|
||||
// 750ms later. Correct behavior would be to still run the job again in
|
||||
// 250ms, but the bug would cause it to run instead 1s later.
|
||||
|
||||
var calls int
|
||||
var mu sync.Mutex
|
||||
|
||||
cron := newWithSeconds()
|
||||
hourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {}))
|
||||
cron.Schedule(Every(time.Second), FuncJob(func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
switch calls {
|
||||
case 0:
|
||||
wg1.Done()
|
||||
calls++
|
||||
case 1:
|
||||
time.Sleep(750 * time.Millisecond)
|
||||
cron.Remove(hourJob)
|
||||
calls++
|
||||
case 2:
|
||||
calls++
|
||||
wg2.Done()
|
||||
case 3:
|
||||
panic("unexpected 3rd call")
|
||||
}
|
||||
}))
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
// the first run might be any length of time 0 - 1s, since the schedule
|
||||
// rounds to the second. wait for the first run to true up.
|
||||
wg1.Wait()
|
||||
|
||||
select {
|
||||
case <-time.After(2 * OneSecond):
|
||||
t.Error("expected job fires 2 times")
|
||||
case <-wait(&wg2):
|
||||
}
|
||||
}
|
||||
|
||||
type ZeroSchedule struct{}
|
||||
|
||||
func (*ZeroSchedule) Next(time.Time) time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Tests that job without time does not run
|
||||
func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
var calls int64
|
||||
cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
|
||||
cron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") }))
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
<-time.After(OneSecond)
|
||||
if atomic.LoadInt64(&calls) != 1 {
|
||||
t.Errorf("called %d times, expected 1\n", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopAndWait(t *testing.T) {
|
||||
t.Run("nothing running, returns immediately", func(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
ctx := cron.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("context was not done immediately")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("repeated calls to Stop", func(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.Start()
|
||||
_ = cron.Stop()
|
||||
time.Sleep(time.Millisecond)
|
||||
ctx := cron.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("context was not done immediately")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a couple fast jobs added, still returns immediately", func(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
cron.Start()
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
time.Sleep(time.Second)
|
||||
ctx := cron.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("context was not done immediately")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a couple fast jobs and a slow job added, waits for slow job", func(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
cron.Start()
|
||||
cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) })
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
time.Sleep(time.Second)
|
||||
|
||||
ctx := cron.Stop()
|
||||
|
||||
// Verify that it is not done for at least 750ms
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Error("context was done too quickly immediately")
|
||||
case <-time.After(750 * time.Millisecond):
|
||||
// expected, because the job sleeping for 1 second is still running
|
||||
}
|
||||
|
||||
// Verify that it IS done in the next 500ms (giving 250ms buffer)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// expected
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
t.Error("context not done after job should have completed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("repeated calls to stop, waiting for completion and after", func(t *testing.T) {
|
||||
cron := newWithSeconds()
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) })
|
||||
cron.Start()
|
||||
cron.AddFunc("* * * * * *", func() {})
|
||||
time.Sleep(time.Second)
|
||||
ctx := cron.Stop()
|
||||
ctx2 := cron.Stop()
|
||||
|
||||
// Verify that it is not done for at least 1500ms
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Error("context was done too quickly immediately")
|
||||
case <-ctx2.Done():
|
||||
t.Error("context2 was done too quickly immediately")
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
// expected, because the job sleeping for 2 seconds is still running
|
||||
}
|
||||
|
||||
// Verify that it IS done in the next 1s (giving 500ms buffer)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// expected
|
||||
case <-time.After(time.Second):
|
||||
t.Error("context not done after job should have completed")
|
||||
}
|
||||
|
||||
// Verify that ctx2 is also done.
|
||||
select {
|
||||
case <-ctx2.Done():
|
||||
// expected
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("context2 not done even though context1 is")
|
||||
}
|
||||
|
||||
// Verify that a new context retrieved from stop is immediately done.
|
||||
ctx3 := cron.Stop()
|
||||
select {
|
||||
case <-ctx3.Done():
|
||||
// expected
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("context not done even when cron Stop is completed")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiThreadedStartAndStop(t *testing.T) {
|
||||
cron := New()
|
||||
go cron.Run()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
cron.Stop()
|
||||
}
|
||||
|
||||
func wait(wg *sync.WaitGroup) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
ch <- true
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func stop(cron *Cron) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
cron.Stop()
|
||||
ch <- true
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// newWithSeconds returns a Cron with the seconds field enabled.
|
||||
func newWithSeconds() *Cron {
|
||||
return New(WithParser(secondParser), WithChain())
|
||||
}
|
||||
@ -1,212 +0,0 @@
|
||||
/*
|
||||
Package cron implements a cron spec parser and job runner.
|
||||
|
||||
Usage
|
||||
|
||||
Callers may register Funcs to be invoked on a given schedule. Cron will run
|
||||
them in their own goroutines.
|
||||
|
||||
c := cron.New()
|
||||
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
|
||||
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
|
||||
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
|
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour, starting an hour from now") })
|
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") })
|
||||
c.Start()
|
||||
..
|
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
...
|
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") })
|
||||
..
|
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries())
|
||||
..
|
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
CRON Expression Format
|
||||
|
||||
A cron expression represents a set of times, using 5 space-separated fields.
|
||||
|
||||
Field name | Mandatory? | Allowed values | Allowed special characters
|
||||
---------- | ---------- | -------------- | --------------------------
|
||||
Minutes | Yes | 0-59 | * / , -
|
||||
Hours | Yes | 0-23 | * / , -
|
||||
Day of month | Yes | 1-31 | * / , - ?
|
||||
Month | Yes | 1-12 or JAN-DEC | * / , -
|
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
|
||||
|
||||
Month and Day-of-week field values are case insensitive. "SUN", "Sun", and
|
||||
"sun" are equally accepted.
|
||||
|
||||
The specific interpretation of the format is based on the Cron Wikipedia page:
|
||||
https://en.wikipedia.org/wiki/Cron
|
||||
|
||||
Alternative Formats
|
||||
|
||||
Alternative Cron expression formats support other fields like seconds. You can
|
||||
implement that by creating a custom Parser as follows.
|
||||
|
||||
cron.New(
|
||||
cron.WithParser(
|
||||
cron.SecondOptional | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor))
|
||||
|
||||
The most popular alternative Cron expression format is Quartz:
|
||||
http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
|
||||
|
||||
Special Characters
|
||||
|
||||
Asterisk ( * )
|
||||
|
||||
The asterisk indicates that the cron expression will match for all values of the
|
||||
field; e.g., using an asterisk in the 5th field (month) would indicate every
|
||||
month.
|
||||
|
||||
Slash ( / )
|
||||
|
||||
Slashes are used to describe increments of ranges. For example 3-59/15 in the
|
||||
1st field (minutes) would indicate the 3rd minute of the hour and every 15
|
||||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...",
|
||||
that is, an increment over the largest possible range of the field. The form
|
||||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
|
||||
increment until the end of that specific range. It does not wrap around.
|
||||
|
||||
Comma ( , )
|
||||
|
||||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
|
||||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
|
||||
|
||||
Hyphen ( - )
|
||||
|
||||
Hyphens are used to define ranges. For example, 9-17 would indicate every
|
||||
hour between 9am and 5pm inclusive.
|
||||
|
||||
Question mark ( ? )
|
||||
|
||||
Question mark may be used instead of '*' for leaving either day-of-month or
|
||||
day-of-week blank.
|
||||
|
||||
Predefined schedules
|
||||
|
||||
You may use one of several pre-defined schedules in place of a cron expression.
|
||||
|
||||
Entry | Description | Equivalent To
|
||||
----- | ----------- | -------------
|
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 1 1 *
|
||||
@monthly | Run once a month, midnight, first of month | 0 0 1 * *
|
||||
@weekly | Run once a week, midnight between Sat/Sun | 0 0 * * 0
|
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 * * *
|
||||
@hourly | Run once an hour, beginning of hour | 0 * * * *
|
||||
|
||||
Intervals
|
||||
|
||||
You may also schedule a job to execute at fixed intervals, starting at the time it's added
|
||||
or cron is run. This is supported by formatting the cron spec like this:
|
||||
|
||||
@every <duration>
|
||||
|
||||
where "duration" is a string accepted by time.ParseDuration
|
||||
(http://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates after
|
||||
1 hour, 30 minutes, 10 seconds, and then every interval after that.
|
||||
|
||||
Note: The interval does not take the job runtime into account. For example,
|
||||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,
|
||||
it will have only 2 minutes of idle time between each run.
|
||||
|
||||
Time zones
|
||||
|
||||
By default, all interpretation and scheduling is done in the machine's local
|
||||
time zone (time.Local). You can specify a different time zone on construction:
|
||||
|
||||
cron.New(
|
||||
cron.WithLocation(time.UTC))
|
||||
|
||||
Individual cron schedules may also override the time zone they are to be
|
||||
interpreted in by providing an additional space-separated field at the beginning
|
||||
of the cron spec, of the form "CRON_TZ=Asia/Tokyo".
|
||||
|
||||
For example:
|
||||
|
||||
# Runs at 6am in time.Local
|
||||
cron.New().AddFunc("0 6 * * ?", ...)
|
||||
|
||||
# Runs at 6am in America/New_York
|
||||
nyc, _ := time.LoadLocation("America/New_York")
|
||||
c := cron.New(cron.WithLocation(nyc))
|
||||
c.AddFunc("0 6 * * ?", ...)
|
||||
|
||||
# Runs at 6am in Asia/Tokyo
|
||||
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
|
||||
|
||||
# Runs at 6am in Asia/Tokyo
|
||||
c := cron.New(cron.WithLocation(nyc))
|
||||
c.SetLocation("America/New_York")
|
||||
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
|
||||
|
||||
The prefix "TZ=(TIME ZONE)" is also supported for legacy compatibility.
|
||||
|
||||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
|
||||
not be run!
|
||||
|
||||
Job Wrappers / Chain
|
||||
|
||||
A Cron runner may be configured with a chain of job wrappers to add
|
||||
cross-cutting functionality to all submitted jobs. For example, they may be used
|
||||
to achieve the following effects:
|
||||
|
||||
- Recover any panics from jobs (activated by default)
|
||||
- Delay a job's execution if the previous run hasn't completed yet
|
||||
- Skip a job's execution if the previous run hasn't completed yet
|
||||
- Log each job's invocations
|
||||
|
||||
Install wrappers for all jobs added to a cron using the `cron.WithChain` option:
|
||||
|
||||
cron.New(cron.WithChain(
|
||||
cron.SkipIfStillRunning(logger),
|
||||
))
|
||||
|
||||
Install wrappers for individual jobs by explicitly wrapping them:
|
||||
|
||||
job = cron.NewChain(
|
||||
cron.SkipIfStillRunning(logger),
|
||||
).Then(job)
|
||||
|
||||
Thread safety
|
||||
|
||||
Since the Cron service runs concurrently with the calling code, some amount of
|
||||
care must be taken to ensure proper synchronization.
|
||||
|
||||
All cron methods are designed to be correctly synchronized as long as the caller
|
||||
ensures that invocations have a clear happens-before ordering between them.
|
||||
|
||||
Logging
|
||||
|
||||
Cron defines a Logger interface that is a subset of the one defined in
|
||||
github.com/go-logr/logr. It has two logging levels (Info and Error), and
|
||||
parameters are key/value pairs. This makes it possible for cron logging to plug
|
||||
into structured logging systems. An adapter, [Verbose]PrintfLogger, is provided
|
||||
to wrap the standard library *log.Logger.
|
||||
|
||||
For additional insight into Cron operations, verbose logging may be activated
|
||||
which will record job runs, scheduling decisions, and added or removed jobs.
|
||||
Activate it with a one-off logger as follows:
|
||||
|
||||
cron.New(
|
||||
cron.WithLogger(
|
||||
cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
|
||||
|
||||
|
||||
Implementation
|
||||
|
||||
Cron entries are stored in an array, sorted by their next activation time. Cron
|
||||
sleeps until the next job is due to be run.
|
||||
|
||||
Upon waking:
|
||||
- it runs each entry that is active on that second
|
||||
- it calculates the next run times for the jobs that were run
|
||||
- it re-sorts the array of entries by next activation time.
|
||||
- it goes to sleep until the soonest job.
|
||||
*/
|
||||
package cron
|
||||
@ -1,86 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultLogger is used by Cron if none is specified.
|
||||
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
|
||||
|
||||
// DiscardLogger can be used by callers to discard all log messages.
|
||||
var DiscardLogger Logger = PrintfLogger(log.New(ioutil.Discard, "", 0))
|
||||
|
||||
// Logger is the interface used in this package for logging, so that any backend
|
||||
// can be plugged in. It is a subset of the github.com/go-logr/logr interface.
|
||||
type Logger interface {
|
||||
// Info logs routine messages about cron's operation.
|
||||
Info(msg string, keysAndValues ...interface{})
|
||||
// Error logs an error condition.
|
||||
Error(err error, msg string, keysAndValues ...interface{})
|
||||
}
|
||||
|
||||
// PrintfLogger wraps a Printf-based logger (such as the standard library "log")
|
||||
// into an implementation of the Logger interface which logs errors only.
|
||||
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
|
||||
return printfLogger{l, false}
|
||||
}
|
||||
|
||||
// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library
|
||||
// "log") into an implementation of the Logger interface which logs everything.
|
||||
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
|
||||
return printfLogger{l, true}
|
||||
}
|
||||
|
||||
type printfLogger struct {
|
||||
logger interface{ Printf(string, ...interface{}) }
|
||||
logInfo bool
|
||||
}
|
||||
|
||||
func (pl printfLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
if pl.logInfo {
|
||||
keysAndValues = formatTimes(keysAndValues)
|
||||
pl.logger.Printf(
|
||||
formatString(len(keysAndValues)),
|
||||
append([]interface{}{msg}, keysAndValues...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
keysAndValues = formatTimes(keysAndValues)
|
||||
pl.logger.Printf(
|
||||
formatString(len(keysAndValues)+2),
|
||||
append([]interface{}{msg, "error", err}, keysAndValues...)...)
|
||||
}
|
||||
|
||||
// formatString returns a logfmt-like format string for the number of
|
||||
// key/values.
|
||||
func formatString(numKeysAndValues int) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("%s")
|
||||
if numKeysAndValues > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
for i := 0; i < numKeysAndValues/2; i++ {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString("%v=%v")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatTimes formats any time.Time values as RFC3339.
|
||||
func formatTimes(keysAndValues []interface{}) []interface{} {
|
||||
var formattedArgs []interface{}
|
||||
for _, arg := range keysAndValues {
|
||||
if t, ok := arg.(time.Time); ok {
|
||||
arg = t.Format(time.RFC3339)
|
||||
}
|
||||
formattedArgs = append(formattedArgs, arg)
|
||||
}
|
||||
return formattedArgs
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Option represents a modification to the default behavior of a Cron.
|
||||
type Option func(*Cron)
|
||||
|
||||
// WithLocation overrides the timezone of the cron instance.
|
||||
func WithLocation(loc *time.Location) Option {
|
||||
return func(c *Cron) {
|
||||
c.location = loc
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeconds overrides the parser used for interpreting job schedules to
|
||||
// include a seconds field as the first one.
|
||||
func WithSeconds() Option {
|
||||
return WithParser(NewParser(
|
||||
Second | Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
))
|
||||
}
|
||||
|
||||
// WithParser overrides the parser used for interpreting job schedules.
|
||||
func WithParser(p Parser) Option {
|
||||
return func(c *Cron) {
|
||||
c.parser = p
|
||||
}
|
||||
}
|
||||
|
||||
// WithChain specifies Job wrappers to apply to all jobs added to this cron.
|
||||
// Refer to the Chain* functions in this package for provided wrappers.
|
||||
func WithChain(wrappers ...JobWrapper) Option {
|
||||
return func(c *Cron) {
|
||||
c.chain = NewChain(wrappers...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger uses the provided logger.
|
||||
func WithLogger(logger Logger) Option {
|
||||
return func(c *Cron) {
|
||||
c.logger = logger
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWithLocation(t *testing.T) {
|
||||
c := New(WithLocation(time.UTC))
|
||||
if c.location != time.UTC {
|
||||
t.Errorf("expected UTC, got %v", c.location)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithParser(t *testing.T) {
|
||||
var parser = NewParser(Dow)
|
||||
c := New(WithParser(parser))
|
||||
if c.parser != parser {
|
||||
t.Error("expected provided parser")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithVerboseLogger(t *testing.T) {
|
||||
var buf syncWriter
|
||||
var logger = log.New(&buf, "", log.LstdFlags)
|
||||
c := New(WithLogger(VerbosePrintfLogger(logger)))
|
||||
if c.logger.(printfLogger).logger != logger {
|
||||
t.Error("expected provided logger")
|
||||
}
|
||||
|
||||
c.AddFunc("@every 1s", func() {})
|
||||
c.Start()
|
||||
time.Sleep(OneSecond)
|
||||
c.Stop()
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "schedule,") ||
|
||||
!strings.Contains(out, "run,") {
|
||||
t.Error("expected to see some actions, got:", out)
|
||||
}
|
||||
}
|
||||
@ -1,435 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Configuration options for creating a parser. Most options specify which
|
||||
// fields should be included, while others enable features. If a field is not
|
||||
// included the parser will assume a default value. These options do not change
|
||||
// the order fields are parse in.
|
||||
type ParseOption int
|
||||
|
||||
const (
|
||||
Second ParseOption = 1 << iota // Seconds field, default 0
|
||||
SecondOptional // Optional seconds field, default 0
|
||||
Minute // Minutes field, default 0
|
||||
Hour // Hours field, default 0
|
||||
Dom // Day of month field, default *
|
||||
Month // Month field, default *
|
||||
Dow // Day of week field, default *
|
||||
DowOptional // Optional day of week field, default *
|
||||
Descriptor // Allow descriptors such as @monthly, @weekly, etc.
|
||||
)
|
||||
|
||||
var places = []ParseOption{
|
||||
Second,
|
||||
Minute,
|
||||
Hour,
|
||||
Dom,
|
||||
Month,
|
||||
Dow,
|
||||
}
|
||||
|
||||
var defaults = []string{
|
||||
"0",
|
||||
"0",
|
||||
"0",
|
||||
"*",
|
||||
"*",
|
||||
"*",
|
||||
}
|
||||
|
||||
// A custom Parser that can be configured.
|
||||
type Parser struct {
|
||||
options ParseOption
|
||||
}
|
||||
|
||||
// NewParser creates a Parser with custom options.
|
||||
//
|
||||
// It panics if more than one Optional is given, since it would be impossible to
|
||||
// correctly infer which optional is provided or missing in general.
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// // Standard parser without descriptors
|
||||
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("0 0 15 */3 *")
|
||||
//
|
||||
// // Same as above, just excludes time fields
|
||||
// subsParser := NewParser(Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("15 */3 *")
|
||||
//
|
||||
// // Same as above, just makes Dow optional
|
||||
// subsParser := NewParser(Dom | Month | DowOptional)
|
||||
// sched, err := specParser.Parse("15 */3")
|
||||
func NewParser(options ParseOption) Parser {
|
||||
optionals := 0
|
||||
if options&DowOptional > 0 {
|
||||
optionals++
|
||||
}
|
||||
if options&SecondOptional > 0 {
|
||||
optionals++
|
||||
}
|
||||
if optionals > 1 {
|
||||
panic("multiple optionals may not be configured")
|
||||
}
|
||||
return Parser{options}
|
||||
}
|
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
// It accepts crontab specs and features configured by NewParser.
|
||||
func (p Parser) Parse(spec string) (Schedule, error) {
|
||||
if len(spec) == 0 {
|
||||
return nil, fmt.Errorf("empty spec string")
|
||||
}
|
||||
|
||||
// Extract timezone if present
|
||||
var loc = time.Local
|
||||
if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {
|
||||
var err error
|
||||
i := strings.Index(spec, " ")
|
||||
eq := strings.Index(spec, "=")
|
||||
if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {
|
||||
return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err)
|
||||
}
|
||||
spec = strings.TrimSpace(spec[i:])
|
||||
}
|
||||
|
||||
// Handle named schedules (descriptors), if configured
|
||||
if strings.HasPrefix(spec, "@") {
|
||||
if p.options&Descriptor == 0 {
|
||||
return nil, fmt.Errorf("parser does not accept descriptors: %v", spec)
|
||||
}
|
||||
return parseDescriptor(spec, loc)
|
||||
}
|
||||
|
||||
// Split on whitespace.
|
||||
fields := strings.Fields(spec)
|
||||
|
||||
// Validate & fill in any omitted or optional fields
|
||||
var err error
|
||||
fields, err = normalizeFields(fields, p.options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
field := func(field string, r bounds) uint64 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var bits uint64
|
||||
bits, err = getField(field, r)
|
||||
return bits
|
||||
}
|
||||
|
||||
var (
|
||||
second = field(fields[0], seconds)
|
||||
minute = field(fields[1], minutes)
|
||||
hour = field(fields[2], hours)
|
||||
dayofmonth = field(fields[3], dom)
|
||||
month = field(fields[4], months)
|
||||
dayofweek = field(fields[5], dow)
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SpecSchedule{
|
||||
Second: second,
|
||||
Minute: minute,
|
||||
Hour: hour,
|
||||
Dom: dayofmonth,
|
||||
Month: month,
|
||||
Dow: dayofweek,
|
||||
Location: loc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normalizeFields takes a subset set of the time fields and returns the full set
|
||||
// with defaults (zeroes) populated for unset fields.
|
||||
//
|
||||
// As part of performing this function, it also validates that the provided
|
||||
// fields are compatible with the configured options.
|
||||
func normalizeFields(fields []string, options ParseOption) ([]string, error) {
|
||||
// Validate optionals & add their field to options
|
||||
optionals := 0
|
||||
if options&SecondOptional > 0 {
|
||||
options |= Second
|
||||
optionals++
|
||||
}
|
||||
if options&DowOptional > 0 {
|
||||
options |= Dow
|
||||
optionals++
|
||||
}
|
||||
if optionals > 1 {
|
||||
return nil, fmt.Errorf("multiple optionals may not be configured")
|
||||
}
|
||||
|
||||
// Figure out how many fields we need
|
||||
max := 0
|
||||
for _, place := range places {
|
||||
if options&place > 0 {
|
||||
max++
|
||||
}
|
||||
}
|
||||
min := max - optionals
|
||||
|
||||
// Validate number of fields
|
||||
if count := len(fields); count < min || count > max {
|
||||
if min == max {
|
||||
return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields)
|
||||
}
|
||||
return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields)
|
||||
}
|
||||
|
||||
// Populate the optional field if not provided
|
||||
if min < max && len(fields) == min {
|
||||
switch {
|
||||
case options&DowOptional > 0:
|
||||
fields = append(fields, defaults[5])
|
||||
case options&SecondOptional > 0:
|
||||
fields = append([]string{defaults[0]}, fields...)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown optional field")
|
||||
}
|
||||
}
|
||||
|
||||
// Populate all fields not part of options with their defaults
|
||||
n := 0
|
||||
expandedFields := make([]string, len(places))
|
||||
copy(expandedFields, defaults)
|
||||
for i, place := range places {
|
||||
if options&place > 0 {
|
||||
expandedFields[i] = fields[n]
|
||||
n++
|
||||
}
|
||||
}
|
||||
return expandedFields, nil
|
||||
}
|
||||
|
||||
var standardParser = NewParser(
|
||||
Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
)
|
||||
|
||||
// ParseStandard returns a new crontab schedule representing the given
|
||||
// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries
|
||||
// representing: minute, hour, day of month, month and day of week, in that
|
||||
// order. It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Standard crontab specs, e.g. "* * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func ParseStandard(standardSpec string) (Schedule, error) {
|
||||
return standardParser.Parse(standardSpec)
|
||||
}
|
||||
|
||||
// getField returns an Int with the bits set representing all of the times that
|
||||
// the field represents or error parsing field value. A "field" is a comma-separated
|
||||
// list of "ranges".
|
||||
func getField(field string, r bounds) (uint64, error) {
|
||||
var bits uint64
|
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
|
||||
for _, expr := range ranges {
|
||||
bit, err := getRange(expr, r)
|
||||
if err != nil {
|
||||
return bits, err
|
||||
}
|
||||
bits |= bit
|
||||
}
|
||||
return bits, nil
|
||||
}
|
||||
|
||||
// getRange returns the bits indicated by the given expression:
|
||||
//
|
||||
// number | number "-" number [ "/" number ]
|
||||
//
|
||||
// or error parsing range.
|
||||
func getRange(expr string, r bounds) (uint64, error) {
|
||||
var (
|
||||
start, end, step uint
|
||||
rangeAndStep = strings.Split(expr, "/")
|
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-")
|
||||
singleDigit = len(lowAndHigh) == 1
|
||||
err error
|
||||
)
|
||||
|
||||
var extra uint64
|
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
|
||||
start = r.min
|
||||
end = r.max
|
||||
extra = starBit
|
||||
} else {
|
||||
start, err = parseIntOrName(lowAndHigh[0], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch len(lowAndHigh) {
|
||||
case 1:
|
||||
end = start
|
||||
case 2:
|
||||
end, err = parseIntOrName(lowAndHigh[1], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("too many hyphens: %s", expr)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(rangeAndStep) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
step, err = mustParseInt(rangeAndStep[1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit {
|
||||
end = r.max
|
||||
}
|
||||
if step > 1 {
|
||||
extra = 0
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("too many slashes: %s", expr)
|
||||
}
|
||||
|
||||
if start < r.min {
|
||||
return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
|
||||
}
|
||||
if end > r.max {
|
||||
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
|
||||
}
|
||||
if start > end {
|
||||
return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
|
||||
}
|
||||
if step == 0 {
|
||||
return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
|
||||
}
|
||||
|
||||
return getBits(start, end, step) | extra, nil
|
||||
}
|
||||
|
||||
// parseIntOrName returns the (possibly-named) integer contained in expr.
|
||||
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
|
||||
if names != nil {
|
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok {
|
||||
return namedInt, nil
|
||||
}
|
||||
}
|
||||
return mustParseInt(expr)
|
||||
}
|
||||
|
||||
// mustParseInt parses the given expression as an int or returns an error.
|
||||
func mustParseInt(expr string) (uint, error) {
|
||||
num, err := strconv.Atoi(expr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
|
||||
}
|
||||
if num < 0 {
|
||||
return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
|
||||
}
|
||||
|
||||
return uint(num), nil
|
||||
}
|
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 {
|
||||
var bits uint64
|
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 {
|
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
|
||||
}
|
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step {
|
||||
bits |= 1 << i
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
// all returns all bits within the given bounds. (plus the star bit)
|
||||
func all(r bounds) uint64 {
|
||||
return getBits(r.min, r.max, 1) | starBit
|
||||
}
|
||||
|
||||
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
|
||||
func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) {
|
||||
switch descriptor {
|
||||
case "@yearly", "@annually":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: 1 << months.min,
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@monthly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@weekly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: 1 << dow.min,
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@daily", "@midnight":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@hourly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: all(hours),
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
const every = "@every "
|
||||
if strings.HasPrefix(descriptor, every) {
|
||||
duration, err := time.ParseDuration(descriptor[len(every):])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err)
|
||||
}
|
||||
return Every(duration), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor)
|
||||
}
|
||||
@ -1,383 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
zero := uint64(0)
|
||||
ranges := []struct {
|
||||
expr string
|
||||
min, max uint
|
||||
expected uint64
|
||||
err string
|
||||
}{
|
||||
{"5", 0, 7, 1 << 5, ""},
|
||||
{"0", 0, 7, 1 << 0, ""},
|
||||
{"7", 0, 7, 1 << 7, ""},
|
||||
|
||||
{"5-5", 0, 7, 1 << 5, ""},
|
||||
{"5-6", 0, 7, 1<<5 | 1<<6, ""},
|
||||
{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
|
||||
|
||||
{"5-6/2", 0, 7, 1 << 5, ""},
|
||||
{"5-7/2", 0, 7, 1<<5 | 1<<7, ""},
|
||||
{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
|
||||
|
||||
{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""},
|
||||
{"*/2", 1, 3, 1<<1 | 1<<3, ""},
|
||||
|
||||
{"5--5", 0, 0, zero, "too many hyphens"},
|
||||
{"jan-x", 0, 0, zero, "failed to parse int from"},
|
||||
{"2-x", 1, 5, zero, "failed to parse int from"},
|
||||
{"*/-12", 0, 0, zero, "negative number"},
|
||||
{"*//2", 0, 0, zero, "too many slashes"},
|
||||
{"1", 3, 5, zero, "below minimum"},
|
||||
{"6", 3, 5, zero, "above maximum"},
|
||||
{"5-3", 3, 5, zero, "beyond end of range"},
|
||||
{"*/0", 0, 0, zero, "should be a positive number"},
|
||||
}
|
||||
|
||||
for _, c := range ranges {
|
||||
actual, err := getRange(c.expr, bounds{c.min, c.max, nil})
|
||||
if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
|
||||
t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
|
||||
}
|
||||
if len(c.err) == 0 && err != nil {
|
||||
t.Errorf("%s => unexpected error %v", c.expr, err)
|
||||
}
|
||||
if actual != c.expected {
|
||||
t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestField(t *testing.T) {
|
||||
fields := []struct {
|
||||
expr string
|
||||
min, max uint
|
||||
expected uint64
|
||||
}{
|
||||
{"5", 1, 7, 1 << 5},
|
||||
{"5,6", 1, 7, 1<<5 | 1<<6},
|
||||
{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
|
||||
{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
|
||||
}
|
||||
|
||||
for _, c := range fields {
|
||||
actual, _ := getField(c.expr, bounds{c.min, c.max, nil})
|
||||
if actual != c.expected {
|
||||
t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
allBits := []struct {
|
||||
r bounds
|
||||
expected uint64
|
||||
}{
|
||||
{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
|
||||
{hours, 0xffffff}, // 0-23: 24 ones
|
||||
{dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero
|
||||
{months, 0x1ffe}, // 1-12: 12 ones, 1 zero
|
||||
{dow, 0x7f}, // 0-6: 7 ones
|
||||
}
|
||||
|
||||
for _, c := range allBits {
|
||||
actual := all(c.r) // all() adds the starBit, so compensate for that..
|
||||
if c.expected|starBit != actual {
|
||||
t.Errorf("%d-%d/%d => expected %b, got %b",
|
||||
c.r.min, c.r.max, 1, c.expected|starBit, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBits(t *testing.T) {
|
||||
bits := []struct {
|
||||
min, max, step uint
|
||||
expected uint64
|
||||
}{
|
||||
{0, 0, 1, 0x1},
|
||||
{1, 1, 1, 0x2},
|
||||
{1, 5, 2, 0x2a}, // 101010
|
||||
{1, 4, 2, 0xa}, // 1010
|
||||
}
|
||||
|
||||
for _, c := range bits {
|
||||
actual := getBits(c.min, c.max, c.step)
|
||||
if c.expected != actual {
|
||||
t.Errorf("%d-%d/%d => expected %b, got %b",
|
||||
c.min, c.max, c.step, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseScheduleErrors(t *testing.T) {
|
||||
var tests = []struct{ expr, err string }{
|
||||
{"* 5 j * * *", "failed to parse int from"},
|
||||
{"@every Xm", "failed to parse duration"},
|
||||
{"@unrecognized", "unrecognized descriptor"},
|
||||
{"* * * *", "expected 5 to 6 fields"},
|
||||
{"", "empty spec string"},
|
||||
}
|
||||
for _, c := range tests {
|
||||
actual, err := secondParser.Parse(c.expr)
|
||||
if err == nil || !strings.Contains(err.Error(), c.err) {
|
||||
t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
|
||||
}
|
||||
if actual != nil {
|
||||
t.Errorf("expected nil schedule on error, got %v", actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSchedule(t *testing.T) {
|
||||
tokyo, _ := time.LoadLocation("Asia/Tokyo")
|
||||
entries := []struct {
|
||||
parser Parser
|
||||
expr string
|
||||
expected Schedule
|
||||
}{
|
||||
{secondParser, "0 5 * * * *", every5min(time.Local)},
|
||||
{standardParser, "5 * * * *", every5min(time.Local)},
|
||||
{secondParser, "CRON_TZ=UTC 0 5 * * * *", every5min(time.UTC)},
|
||||
{standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)},
|
||||
{secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)},
|
||||
{secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},
|
||||
{secondParser, "@midnight", midnight(time.Local)},
|
||||
{secondParser, "TZ=UTC @midnight", midnight(time.UTC)},
|
||||
{secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)},
|
||||
{secondParser, "@yearly", annual(time.Local)},
|
||||
{secondParser, "@annually", annual(time.Local)},
|
||||
{
|
||||
parser: secondParser,
|
||||
expr: "* 5 * * * *",
|
||||
expected: &SpecSchedule{
|
||||
Second: all(seconds),
|
||||
Minute: 1 << 5,
|
||||
Hour: all(hours),
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: time.Local,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range entries {
|
||||
actual, err := c.parser.Parse(c.expr)
|
||||
if err != nil {
|
||||
t.Errorf("%s => unexpected error %v", c.expr, err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, c.expected) {
|
||||
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalSecondSchedule(t *testing.T) {
|
||||
parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor)
|
||||
entries := []struct {
|
||||
expr string
|
||||
expected Schedule
|
||||
}{
|
||||
{"0 5 * * * *", every5min(time.Local)},
|
||||
{"5 5 * * * *", every5min5s(time.Local)},
|
||||
{"5 * * * *", every5min(time.Local)},
|
||||
}
|
||||
|
||||
for _, c := range entries {
|
||||
actual, err := parser.Parse(c.expr)
|
||||
if err != nil {
|
||||
t.Errorf("%s => unexpected error %v", c.expr, err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, c.expected) {
|
||||
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
options ParseOption
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"AllFields_NoOptional",
|
||||
[]string{"0", "5", "*", "*", "*", "*"},
|
||||
Second | Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
[]string{"0", "5", "*", "*", "*", "*"},
|
||||
},
|
||||
{
|
||||
"AllFields_SecondOptional_Provided",
|
||||
[]string{"0", "5", "*", "*", "*", "*"},
|
||||
SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
[]string{"0", "5", "*", "*", "*", "*"},
|
||||
},
|
||||
{
|
||||
"AllFields_SecondOptional_NotProvided",
|
||||
[]string{"5", "*", "*", "*", "*"},
|
||||
SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
[]string{"0", "5", "*", "*", "*", "*"},
|
||||
},
|
||||
{
|
||||
"SubsetFields_NoOptional",
|
||||
[]string{"5", "15", "*"},
|
||||
Hour | Dom | Month,
|
||||
[]string{"0", "0", "5", "15", "*", "*"},
|
||||
},
|
||||
{
|
||||
"SubsetFields_DowOptional_Provided",
|
||||
[]string{"5", "15", "*", "4"},
|
||||
Hour | Dom | Month | DowOptional,
|
||||
[]string{"0", "0", "5", "15", "*", "4"},
|
||||
},
|
||||
{
|
||||
"SubsetFields_DowOptional_NotProvided",
|
||||
[]string{"5", "15", "*"},
|
||||
Hour | Dom | Month | DowOptional,
|
||||
[]string{"0", "0", "5", "15", "*", "*"},
|
||||
},
|
||||
{
|
||||
"SubsetFields_SecondOptional_NotProvided",
|
||||
[]string{"5", "15", "*"},
|
||||
SecondOptional | Hour | Dom | Month,
|
||||
[]string{"0", "0", "5", "15", "*", "*"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual, err := normalizeFields(test.input, test.options)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, test.expected) {
|
||||
t.Errorf("expected %v, got %v", test.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFields_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
options ParseOption
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"TwoOptionals",
|
||||
[]string{"0", "5", "*", "*", "*", "*"},
|
||||
SecondOptional | Minute | Hour | Dom | Month | DowOptional,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"TooManyFields",
|
||||
[]string{"0", "5", "*", "*"},
|
||||
SecondOptional | Minute | Hour,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"NoFields",
|
||||
[]string{},
|
||||
SecondOptional | Minute | Hour,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"TooFewFields",
|
||||
[]string{"*"},
|
||||
SecondOptional | Minute | Hour,
|
||||
"",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual, err := normalizeFields(test.input, test.options)
|
||||
if err == nil {
|
||||
t.Errorf("expected an error, got none. results: %v", actual)
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.err) {
|
||||
t.Errorf("expected error %q, got %q", test.err, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandardSpecSchedule(t *testing.T) {
|
||||
entries := []struct {
|
||||
expr string
|
||||
expected Schedule
|
||||
err string
|
||||
}{
|
||||
{
|
||||
expr: "5 * * * *",
|
||||
expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},
|
||||
},
|
||||
{
|
||||
expr: "@every 5m",
|
||||
expected: ConstantDelaySchedule{time.Duration(5) * time.Minute},
|
||||
},
|
||||
{
|
||||
expr: "5 j * * *",
|
||||
err: "failed to parse int from",
|
||||
},
|
||||
{
|
||||
expr: "* * * *",
|
||||
err: "expected exactly 5 fields",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range entries {
|
||||
actual, err := ParseStandard(c.expr)
|
||||
if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
|
||||
t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
|
||||
}
|
||||
if len(c.err) == 0 && err != nil {
|
||||
t.Errorf("%s => unexpected error %v", c.expr, err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, c.expected) {
|
||||
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoDescriptorParser(t *testing.T) {
|
||||
parser := NewParser(Minute | Hour)
|
||||
_, err := parser.Parse("@every 1m")
|
||||
if err == nil {
|
||||
t.Error("expected an error, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func every5min(loc *time.Location) *SpecSchedule {
|
||||
return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
|
||||
}
|
||||
|
||||
func every5min5s(loc *time.Location) *SpecSchedule {
|
||||
return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
|
||||
}
|
||||
|
||||
func midnight(loc *time.Location) *SpecSchedule {
|
||||
return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}
|
||||
}
|
||||
|
||||
func annual(loc *time.Location) *SpecSchedule {
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: 1 << months.min,
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}
|
||||
}
|
||||
@ -1,188 +0,0 @@
|
||||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
|
||||
// traditional crontab specification. It is computed initially and stored as bit sets.
|
||||
type SpecSchedule struct {
|
||||
Second, Minute, Hour, Dom, Month, Dow uint64
|
||||
|
||||
// Override location for this schedule.
|
||||
Location *time.Location
|
||||
}
|
||||
|
||||
// bounds provides a range of acceptable values (plus a map of name to value).
|
||||
type bounds struct {
|
||||
min, max uint
|
||||
names map[string]uint
|
||||
}
|
||||
|
||||
// The bounds for each field.
|
||||
var (
|
||||
seconds = bounds{0, 59, nil}
|
||||
minutes = bounds{0, 59, nil}
|
||||
hours = bounds{0, 23, nil}
|
||||
dom = bounds{1, 31, nil}
|
||||
months = bounds{1, 12, map[string]uint{
|
||||
"jan": 1,
|
||||
"feb": 2,
|
||||
"mar": 3,
|
||||
"apr": 4,
|
||||
"may": 5,
|
||||
"jun": 6,
|
||||
"jul": 7,
|
||||
"aug": 8,
|
||||
"sep": 9,
|
||||
"oct": 10,
|
||||
"nov": 11,
|
||||
"dec": 12,
|
||||
}}
|
||||
dow = bounds{0, 6, map[string]uint{
|
||||
"sun": 0,
|
||||
"mon": 1,
|
||||
"tue": 2,
|
||||
"wed": 3,
|
||||
"thu": 4,
|
||||
"fri": 5,
|
||||
"sat": 6,
|
||||
}}
|
||||
)
|
||||
|
||||
const (
|
||||
// Set the top bit if a star was included in the expression.
|
||||
starBit = 1 << 63
|
||||
)
|
||||
|
||||
// Next returns the next time this schedule is activated, greater than the given
|
||||
// time. If no time can be found to satisfy the schedule, return the zero time.
|
||||
func (s *SpecSchedule) Next(t time.Time) time.Time {
|
||||
// General approach
|
||||
//
|
||||
// For Month, Day, Hour, Minute, Second:
|
||||
// Check if the time value matches. If yes, continue to the next field.
|
||||
// If the field doesn't match the schedule, then increment the field until it matches.
|
||||
// While incrementing the field, a wrap-around brings it back to the beginning
|
||||
// of the field list (since it is necessary to re-verify previous field
|
||||
// values)
|
||||
|
||||
// Convert the given time into the schedule's timezone, if one is specified.
|
||||
// Save the original timezone so we can convert back after we find a time.
|
||||
// Note that schedules without a time zone specified (time.Local) are treated
|
||||
// as local to the time provided.
|
||||
origLocation := t.Location()
|
||||
loc := s.Location
|
||||
if loc == time.Local {
|
||||
loc = t.Location()
|
||||
}
|
||||
if s.Location != time.Local {
|
||||
t = t.In(s.Location)
|
||||
}
|
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
|
||||
// This flag indicates whether a field has been incremented.
|
||||
added := false
|
||||
|
||||
// If no time is found within five years, return zero.
|
||||
yearLimit := t.Year() + 5
|
||||
|
||||
WRAP:
|
||||
if t.Year() > yearLimit {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Find the first applicable month.
|
||||
// If it's this month, then do nothing.
|
||||
for 1<<uint(t.Month())&s.Month == 0 {
|
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added {
|
||||
added = true
|
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
|
||||
}
|
||||
t = t.AddDate(0, 1, 0)
|
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
// Now get a day in that month.
|
||||
//
|
||||
// NOTE: This causes issues for daylight savings regimes where midnight does
|
||||
// not exist. For example: Sao Paulo has DST that transforms midnight on
|
||||
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
|
||||
for !dayMatches(s, t) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
t = t.AddDate(0, 0, 1)
|
||||
// Notice if the hour is no longer midnight due to DST.
|
||||
// Add an hour if it's 23, subtract an hour if it's 1.
|
||||
if t.Hour() != 0 {
|
||||
if t.Hour() > 12 {
|
||||
t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
|
||||
} else {
|
||||
t = t.Add(time.Duration(-t.Hour()) * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Hour())&s.Hour == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
|
||||
}
|
||||
t = t.Add(1 * time.Hour)
|
||||
|
||||
if t.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Minute())&s.Minute == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Minute)
|
||||
}
|
||||
t = t.Add(1 * time.Minute)
|
||||
|
||||
if t.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Second())&s.Second == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Second)
|
||||
}
|
||||
t = t.Add(1 * time.Second)
|
||||
|
||||
if t.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
return t.In(origLocation)
|
||||
}
|
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func dayMatches(s *SpecSchedule, t time.Time) bool {
|
||||
var (
|
||||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
|
||||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
|
||||
)
|
||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
|
||||
return domMatch && dowMatch
|
||||
}
|
||||
return domMatch || dowMatch
|
||||
}
|
||||
@ -1,300 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestActivation(t *testing.T) {
|
||||
tests := []struct {
|
||||
time, spec string
|
||||
expected bool
|
||||
}{
|
||||
// Every fifteen minutes.
|
||||
{"Mon Jul 9 15:00 2012", "0/15 * * * *", true},
|
||||
{"Mon Jul 9 15:45 2012", "0/15 * * * *", true},
|
||||
{"Mon Jul 9 15:40 2012", "0/15 * * * *", false},
|
||||
|
||||
// Every fifteen minutes, starting at 5 minutes.
|
||||
{"Mon Jul 9 15:05 2012", "5/15 * * * *", true},
|
||||
{"Mon Jul 9 15:20 2012", "5/15 * * * *", true},
|
||||
{"Mon Jul 9 15:50 2012", "5/15 * * * *", true},
|
||||
|
||||
// Named months
|
||||
{"Sun Jul 15 15:00 2012", "0/15 * * Jul *", true},
|
||||
{"Sun Jul 15 15:00 2012", "0/15 * * Jun *", false},
|
||||
|
||||
// Everything set.
|
||||
{"Sun Jul 15 08:30 2012", "30 08 ? Jul Sun", true},
|
||||
{"Sun Jul 15 08:30 2012", "30 08 15 Jul ?", true},
|
||||
{"Mon Jul 16 08:30 2012", "30 08 ? Jul Sun", false},
|
||||
{"Mon Jul 16 08:30 2012", "30 08 15 Jul ?", false},
|
||||
|
||||
// Predefined schedules
|
||||
{"Mon Jul 9 15:00 2012", "@hourly", true},
|
||||
{"Mon Jul 9 15:04 2012", "@hourly", false},
|
||||
{"Mon Jul 9 15:00 2012", "@daily", false},
|
||||
{"Mon Jul 9 00:00 2012", "@daily", true},
|
||||
{"Mon Jul 9 00:00 2012", "@weekly", false},
|
||||
{"Sun Jul 8 00:00 2012", "@weekly", true},
|
||||
{"Sun Jul 8 01:00 2012", "@weekly", false},
|
||||
{"Sun Jul 8 00:00 2012", "@monthly", false},
|
||||
{"Sun Jul 1 00:00 2012", "@monthly", true},
|
||||
|
||||
// Test interaction of DOW and DOM.
|
||||
// If both are restricted, then only one needs to match.
|
||||
{"Sun Jul 15 00:00 2012", "* * 1,15 * Sun", true},
|
||||
{"Fri Jun 15 00:00 2012", "* * 1,15 * Sun", true},
|
||||
{"Wed Aug 1 00:00 2012", "* * 1,15 * Sun", true},
|
||||
{"Sun Jul 15 00:00 2012", "* * */10 * Sun", true}, // verifies #70
|
||||
|
||||
// However, if one has a star, then both need to match.
|
||||
{"Sun Jul 15 00:00 2012", "* * * * Mon", false},
|
||||
{"Mon Jul 9 00:00 2012", "* * 1,15 * *", false},
|
||||
{"Sun Jul 15 00:00 2012", "* * 1,15 * *", true},
|
||||
{"Sun Jul 15 00:00 2012", "* * */2 * Sun", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
sched, err := ParseStandard(test.spec)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
actual := sched.Next(getTime(test.time).Add(-1 * time.Second))
|
||||
expected := getTime(test.time)
|
||||
if test.expected && expected != actual || !test.expected && expected == actual {
|
||||
t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)",
|
||||
test.spec, test.time, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNext(t *testing.T) {
|
||||
runs := []struct {
|
||||
time, spec string
|
||||
expected string
|
||||
}{
|
||||
// Simple cases
|
||||
{"Mon Jul 9 14:45 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"},
|
||||
{"Mon Jul 9 14:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"},
|
||||
{"Mon Jul 9 14:59:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"},
|
||||
|
||||
// Wrap around hours
|
||||
{"Mon Jul 9 15:45 2012", "0 20-35/15 * * * *", "Mon Jul 9 16:20 2012"},
|
||||
|
||||
// Wrap around days
|
||||
{"Mon Jul 9 23:46 2012", "0 */15 * * * *", "Tue Jul 10 00:00 2012"},
|
||||
{"Mon Jul 9 23:45 2012", "0 20-35/15 * * * *", "Tue Jul 10 00:20 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * * *", "Tue Jul 10 00:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * * *", "Tue Jul 10 01:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * * *", "Tue Jul 10 10:20:15 2012"},
|
||||
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"},
|
||||
|
||||
// Wrap around months
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Tue Aug 1 00:00 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"},
|
||||
|
||||
// Wrap around years
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"},
|
||||
|
||||
// Wrap around minute, hour, day, month, and year
|
||||
{"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"},
|
||||
|
||||
// Leap year
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"},
|
||||
|
||||
// Daylight savings time 2am EST (-5) -> 3am EDT (-4)
|
||||
{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"},
|
||||
|
||||
// hourly job
|
||||
{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
|
||||
{"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
|
||||
{"2012-03-11T03:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},
|
||||
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T05:00:00-0400"},
|
||||
|
||||
// hourly job using CRON_TZ
|
||||
{"2012-03-11T00:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
|
||||
{"2012-03-11T01:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
|
||||
{"2012-03-11T03:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},
|
||||
{"2012-03-11T04:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T05:00:00-0400"},
|
||||
|
||||
// 1am nightly job
|
||||
{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-03-11T01:00:00-0500"},
|
||||
{"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-03-12T01:00:00-0400"},
|
||||
|
||||
// 2am nightly job (skipped)
|
||||
{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-03-12T02:00:00-0400"},
|
||||
|
||||
// Daylight savings time 2am EDT (-4) => 1am EST (-5)
|
||||
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"},
|
||||
{"2012-11-04T01:45:00-0400", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
|
||||
|
||||
// hourly job
|
||||
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0400"},
|
||||
{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0500"},
|
||||
{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T02:00:00-0500"},
|
||||
|
||||
// 1am nightly job (runs twice)
|
||||
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
|
||||
{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
|
||||
{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-05T01:00:00-0500"},
|
||||
|
||||
// 2am nightly job
|
||||
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
|
||||
{"2012-11-04T02:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-11-05T02:00:00-0500"},
|
||||
|
||||
// 3am nightly job
|
||||
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
|
||||
{"2012-11-04T03:00:00-0500", "TZ=America/New_York 0 0 3 * * ?", "2012-11-05T03:00:00-0500"},
|
||||
|
||||
// hourly job
|
||||
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0400"},
|
||||
{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0500"},
|
||||
{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T02:00:00-0500"},
|
||||
|
||||
// 1am nightly job (runs twice)
|
||||
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
|
||||
{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
|
||||
{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-05T01:00:00-0500"},
|
||||
|
||||
// 2am nightly job
|
||||
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
|
||||
{"TZ=America/New_York 2012-11-04T02:00:00-0500", "0 0 2 * * ?", "2012-11-05T02:00:00-0500"},
|
||||
|
||||
// 3am nightly job
|
||||
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
|
||||
{"TZ=America/New_York 2012-11-04T03:00:00-0500", "0 0 3 * * ?", "2012-11-05T03:00:00-0500"},
|
||||
|
||||
// Unsatisfiable
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""},
|
||||
|
||||
// Monthly job
|
||||
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 3 * ?", "2012-12-03T03:00:00-0500"},
|
||||
|
||||
// Test the scenario of DST resulting in midnight not being a valid time.
|
||||
// https://github.com/robfig/cron/issues/157
|
||||
{"2018-10-17T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"},
|
||||
{"2018-02-14T05:00:00-0500", "TZ=America/Sao_Paulo 0 0 9 22 * ?", "2018-02-22T07:00:00-0500"},
|
||||
}
|
||||
|
||||
for _, c := range runs {
|
||||
sched, err := secondParser.Parse(c.spec)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
actual := sched.Next(getTime(c.time))
|
||||
expected := getTime(c.expected)
|
||||
if !actual.Equal(expected) {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
invalidSpecs := []string{
|
||||
"xyz",
|
||||
"60 0 * * *",
|
||||
"0 60 * * *",
|
||||
"0 0 * * XYZ",
|
||||
}
|
||||
for _, spec := range invalidSpecs {
|
||||
_, err := ParseStandard(spec)
|
||||
if err == nil {
|
||||
t.Error("expected an error parsing: ", spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTime(value string) time.Time {
|
||||
if value == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
var location = time.Local
|
||||
if strings.HasPrefix(value, "TZ=") {
|
||||
parts := strings.Fields(value)
|
||||
loc, err := time.LoadLocation(parts[0][len("TZ="):])
|
||||
if err != nil {
|
||||
panic("could not parse location:" + err.Error())
|
||||
}
|
||||
location = loc
|
||||
value = parts[1]
|
||||
}
|
||||
|
||||
var layouts = []string{
|
||||
"Mon Jan 2 15:04 2006",
|
||||
"Mon Jan 2 15:04:05 2006",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, value, location); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
if t, err := time.ParseInLocation("2006-01-02T15:04:05-0700", value, location); err == nil {
|
||||
return t
|
||||
}
|
||||
panic("could not parse time value " + value)
|
||||
}
|
||||
|
||||
func TestNextWithTz(t *testing.T) {
|
||||
runs := []struct {
|
||||
time, spec string
|
||||
expected string
|
||||
}{
|
||||
// Failing tests
|
||||
{"2016-01-03T13:09:03+0530", "14 14 * * *", "2016-01-03T14:14:00+0530"},
|
||||
{"2016-01-03T04:09:03+0530", "14 14 * * ?", "2016-01-03T14:14:00+0530"},
|
||||
|
||||
// Passing tests
|
||||
{"2016-01-03T14:09:03+0530", "14 14 * * *", "2016-01-03T14:14:00+0530"},
|
||||
{"2016-01-03T14:00:00+0530", "14 14 * * ?", "2016-01-03T14:14:00+0530"},
|
||||
}
|
||||
for _, c := range runs {
|
||||
sched, err := ParseStandard(c.spec)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
actual := sched.Next(getTimeTZ(c.time))
|
||||
expected := getTimeTZ(c.expected)
|
||||
if !actual.Equal(expected) {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeTZ(value string) time.Time {
|
||||
if value == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
t, err := time.Parse("Mon Jan 2 15:04 2006", value)
|
||||
if err != nil {
|
||||
t, err = time.Parse("Mon Jan 2 15:04:05 2006", value)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02T15:04:05-0700", value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// https://github.com/robfig/cron/issues/144
|
||||
func TestSlash0NoHang(t *testing.T) {
|
||||
schedule := "TZ=America/New_York 15/0 * * * *"
|
||||
_, err := ParseStandard(schedule)
|
||||
if err == nil {
|
||||
t.Error("expected an error on 0 increment")
|
||||
}
|
||||
}
|
||||
@ -7,11 +7,12 @@ import (
|
||||
"server/MergeConst"
|
||||
"server/conf"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
|
||||
// "server/game"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
@ -384,13 +385,15 @@ func InsertServerData(data *SqlServerModStruct) error {
|
||||
}
|
||||
|
||||
func SavePlayerModData(data *SqlModStruct) error {
|
||||
sql := "INSERT INTO `t_player_mod` (`mData` , `updateTime` ,`dwUin`) Values (?,?,?) ON DUPLICATE KEY UPDATE `mData` = ? , `updateTime` = ? "
|
||||
tableName := "t_player_mod_" + fmt.Sprintf("%02d", data.DwUin%100)
|
||||
sql := "INSERT INTO `" + tableName + "` (`mData` , `updateTime` ,`dwUin`) Values (?,?,?) ON DUPLICATE KEY UPDATE `mData` = ? , `updateTime` = ? "
|
||||
_, err := SqlDb.Exec(sql, data.ModData, data.UpdataTime, data.DwUin, data.ModData, data.UpdataTime)
|
||||
return err
|
||||
}
|
||||
|
||||
func InsertPlayerModData(data *SqlModStruct) error {
|
||||
sql := "insert into t_player_mod (`mData` , `updateTime` ,`dwUin`) Values (?,?,?)"
|
||||
tableName := "t_player_mod_" + fmt.Sprintf("%02d", data.DwUin%100)
|
||||
sql := "insert into `" + tableName + "` (`mData` , `updateTime` ,`dwUin`) Values (?,?,?)"
|
||||
_, err := SqlDb.Exec(sql, data.ModData, data.UpdataTime, data.DwUin)
|
||||
return err
|
||||
}
|
||||
@ -414,7 +417,7 @@ func GetServerMailData(data *[]*SqlServerMailStruct) error {
|
||||
}
|
||||
|
||||
func GetActivityData(data *[]*SqlActivityCfgStruct) error {
|
||||
sql := "select `id`, `type`, `title`, `mail_title`, `mail_content`, `level_limit`, `start_time`, `end_time`, `cfg_buf`, `extra` from t_activity_mod"
|
||||
sql := "select `id`, `type`, `title`, `mail_title`, `mail_content`, `level_limit`, `start_time`, `end_time`, `cfg`, `extra`, `interval` from t_activity_mod"
|
||||
err := SqlDb.Select(data, sql)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -3,10 +3,11 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"server/conf"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
|
||||
@ -506,7 +506,8 @@ type SqlActivityCfgStruct struct {
|
||||
Level int `db:"level_limit"`
|
||||
Start_time int64 `db:"start_time"`
|
||||
End_time int64 `db:"end_time"`
|
||||
Cfg []byte `db:"cfg_buf"`
|
||||
Cfg string `db:"cfg"`
|
||||
Interval int64 `db:"interval"`
|
||||
Extra string `db:"extra"`
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package ga
|
||||
|
||||
import (
|
||||
galog "github.com/tuyou/galog"
|
||||
galog "gitea.bywaystudios.com/pet_home/galog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package telog
|
||||
package ga
|
||||
|
||||
import (
|
||||
"server/conf"
|
||||
"server/thinkingdata"
|
||||
|
||||
thinkingdata "gitea.bywaystudios.com/pet_home/thinkdata"
|
||||
)
|
||||
|
||||
var Te thinkingdata.TDAnalytics
|
||||
@ -10,7 +11,6 @@ var Te thinkingdata.TDAnalytics
|
||||
func init() {
|
||||
// 初始化
|
||||
// 创建 LogConfig 配置文件
|
||||
|
||||
config := thinkingdata.TDLogConsumerConfig{
|
||||
Directory: conf.Server.TELOGDIR, // 事件采集的文件路径
|
||||
}
|
||||
7
src/server/galog/.gitignore
vendored
7
src/server/galog/.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
logs/
|
||||
ga_log/
|
||||
asset_log/
|
||||
@ -1,292 +0,0 @@
|
||||
# galog
|
||||
|
||||
galog是一个无三方依赖、支持单CPU最高5wQPS写入的ga-sdk组件,可直接用于GA事实日志和资产日志打点。
|
||||
|
||||
> 说明:默认是异步写入模式,异步队列长度=100,较大QPS时可自行调整,详见参数:
|
||||
> - enableAsync
|
||||
> - defaultAsyncMsgLen
|
||||
|
||||
## Usage
|
||||
|
||||
```golang
|
||||
// ga日志初始化
|
||||
projectID := "20433"
|
||||
clientID := "Android_5.0_tyGuest,weixinPay,tyAccount.alipay.0-hall20433.tuyoo.sdktest"
|
||||
glogger, err := galog.NewGALogger("logs", projectID, clientID, galog.LogTypeTrack)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// ga日志打点调用
|
||||
props := map[string]interface{}{
|
||||
"ip_address": "127.0.0.1",
|
||||
"proj_app_id": "10010",
|
||||
"uuid": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts": tt.UnixNano(),
|
||||
}
|
||||
glogger.
|
||||
GetEntry("sdk_s_login_succ").
|
||||
SetDeviceID("device001").
|
||||
SetUserID("10086").
|
||||
SetProperties(props).
|
||||
Flush()
|
||||
```
|
||||
|
||||
```golang
|
||||
// asset日志初始化
|
||||
projectID := "28"
|
||||
clientID := "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz"
|
||||
glogger, err := galog.NewGALogger("logs", projectID, clientID, galog.LogTypeAsset)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// asset日志打点调用
|
||||
asset := galog.AssetProperties{}
|
||||
asset.
|
||||
SetAssetID("13101").
|
||||
SetAssetName("\u9501\u5b9a").
|
||||
SetAssetType("6").
|
||||
SetAssetFinal("2").
|
||||
SetAssetAssociated("3").
|
||||
SetAssetStartTime("0").
|
||||
SetAssetTimeLimit("0").
|
||||
SetAssetSource("").
|
||||
SetKV("uuid", "uuid-v4")
|
||||
|
||||
glogger.
|
||||
GetEntry("asset_increase").
|
||||
SetDeviceID("").
|
||||
SetUserID("10086").
|
||||
SetProperties(asset).
|
||||
Flush()
|
||||
```
|
||||
|
||||
## suger usage
|
||||
|
||||
```golang
|
||||
package chat_log
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"tygit.tuyoo.com/gocomponents/galog"
|
||||
)
|
||||
|
||||
var (
|
||||
slogger *galog.Logger
|
||||
galogger *galog.GALogger
|
||||
|
||||
lerr error
|
||||
)
|
||||
|
||||
// MustInitLoggerOnce 服务启动后初始化一次
|
||||
func MustInitLoggerOnce() {
|
||||
slogger, lerr = galog.NewServerLogger(&galog.ServerLogOptions{
|
||||
LogDir: "run/logs",
|
||||
EnableAsync: true,
|
||||
AsyncQueueSize: 1000,
|
||||
})
|
||||
if lerr != nil {
|
||||
log.Printf("MustInitLoggerOnce err: %s\n", lerr.Error())
|
||||
panic("MustInitLoggerOnce err")
|
||||
}
|
||||
}
|
||||
|
||||
// MustInitGALoggerOnce 服务启动后初始化一次
|
||||
func MustInitGALoggerOnce() {
|
||||
galogger, lerr = galog.NewServerGALogger(&galog.ServerGALogOptions{
|
||||
LogDir: "run/bi",
|
||||
EnableAsync: true,
|
||||
AsyncQueueSize: 1000,
|
||||
ProjectID: ProjectId,
|
||||
ClientID: ClientId,
|
||||
LogType: galog.LogTypeTrack,
|
||||
})
|
||||
if lerr != nil {
|
||||
log.Printf("MustInitGALoggerOnce err: %s\n", lerr.Error())
|
||||
panic("MustInitGALoggerOnce err")
|
||||
}
|
||||
}
|
||||
|
||||
func LogToGANew(eventName, deviceId string, userId int64, prop map[string]interface{}, version string) {
|
||||
lib := map[string]string{
|
||||
"lib_type": "golang",
|
||||
"lib_version": version,
|
||||
}
|
||||
|
||||
galogger.
|
||||
GetEntry(eventName).
|
||||
SetDeviceID(deviceId).
|
||||
SetUserID(strconv.FormatInt(userId, 10)).
|
||||
SetLib(lib).
|
||||
SetProperties(prop).
|
||||
Flush()
|
||||
}
|
||||
```
|
||||
|
||||
## 日志目录结构
|
||||
|
||||
```
|
||||
{logDir}/ga_log/{日志类型}_{hostname}_{年月日}_{小时}.log
|
||||
{logDir}/asset_log/{日志类型}_{hostname}_{年月日}_{小时}.log
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
- 测试机:Mac-M1 8c 16G
|
||||
|
||||
- 同步写入模式
|
||||
```
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
pkg: tygit.tuyoo.com/gocomponents/galog
|
||||
BenchmarkGaLog-8 87565 12831 ns/op 7590 B/op 90 allocs/op
|
||||
BenchmarkAssetLog-8 234127 5832 ns/op 3028 B/op 44 allocs/op
|
||||
PASS
|
||||
ok tygit.tuyoo.com/gocomponents/galog 3.190s
|
||||
|
||||
# 100w行日志文件,写入性能未见明显衰减
|
||||
BenchmarkGaLog-8 89175 12481 ns/op 7590 B/op 90 allocs/op
|
||||
BenchmarkAssetLog-8 195075 5933 ns/op 3028 B/op 44 allocs/op
|
||||
```
|
||||
|
||||
- 异步写入模式(默认)
|
||||
```
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
pkg: tygit.tuyoo.com/gocomponents/galog
|
||||
BenchmarkGaLog-8 95530 11331 ns/op 7599 B/op 90 allocs/op
|
||||
BenchmarkAssetLog-8 337963 3512 ns/op 3032 B/op 44 allocs/op
|
||||
PASS
|
||||
ok tygit.tuyoo.com/gocomponents/galog 3.040s
|
||||
|
||||
# 120w行日志文件,写入性能未见明显衰减
|
||||
BenchmarkGaLog-8 116559 10450 ns/op 7598 B/op 90 allocs/op
|
||||
BenchmarkAssetLog-8 335670 3512 ns/op 3032 B/op 44 allocs/op
|
||||
```
|
||||
|
||||
## Load Testing
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o galog examples/hey.go
|
||||
|
||||
- 测试机:sa101-ecs-bj4-pt57-test-wxy
|
||||
- linux服务器配置:2c4G
|
||||
- 测试时间:2024-02-26 17:45 ~ 18:45
|
||||
|
||||
```
|
||||
./galog -n 1000000 -c 2 -q 100
|
||||
```
|
||||
|
||||
| 压测说明 | CPU平台使用率 | 内存使用率 | IO平均使用率 | 进程打开的文件句柄数 |
|
||||
|----- | ----- | ----- | ----- | ----- |
|
||||
| 无任务 | 1% | 52% | 0.08% | 0 |
|
||||
| 并发=2,qps=200 | 1% | 52% | 0.08% | 7 |
|
||||
| 并发=5,qps=5000 | 4% | 52% | 1% | 7 |
|
||||
| 并发=5,qps=50000(队列1000,有阻塞) realqps=5000 | 40% | 52% | 30% | 7 |
|
||||
| 并发=5,qps=50000(队列10000,有阻塞) realqps=6500 | 6% | 52% | 1% | 7 |
|
||||
| 并发=20,qps=50000(队列10000,有阻塞) realqps=18000 | 10% | 52% | 3% | 7 |
|
||||
| 并发=50,qps=50000(队列10000) realqps=50000 | 35% | 52% | 10% | 7 |
|
||||
| 并发=100,qps=100000(队列10000) realqps=90000 | 75% | 52% | 16% | 7 |
|
||||
|
||||
- 压测结果:写入队列=10000,单核支持最高写入QPS约5w,CPU=75%。
|
||||
|
||||
- 监控结果如下:
|
||||

|
||||

|
||||
|
||||
## ga日志格式范式
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "20437",
|
||||
"type": "track",
|
||||
"event": "sdk_sword_holder_succ",
|
||||
"event_time": 1708568434744,
|
||||
"device_id": "",
|
||||
"user_id": "908825698",
|
||||
"client_id": "Android_5.1_tyGuest,weixinPay,tyAccount.alipay.0-hall20437.tuyoo.sdkonline",
|
||||
"properties": {
|
||||
"sdk_track_id": "3:6060:636978360:1708568434",
|
||||
"sdk_sub_channel": "fish3d",
|
||||
"country": "中国",
|
||||
"proj_game_id": "",
|
||||
"sdk_error_code": "",
|
||||
"sdk_error_msg": "",
|
||||
"city": "宁德市",
|
||||
"sdk_sword_track_id": "807370ff-ceac-4ee3-b68f-50642ca4953c",
|
||||
"sub_platform_id": "2",
|
||||
"sdk_sword_holder_id": "320",
|
||||
"sdk_sword_version": "v1.1.7",
|
||||
"uuid": "56fdf0f9-9daa-4bf6-be69-88e15f05c36c",
|
||||
"province": "福建省",
|
||||
"sdk_login_channel_type": "",
|
||||
"app_id": "20437",
|
||||
"sdk_s_login_channel_type": "",
|
||||
"sdk_main_channel": "official",
|
||||
"game_id": "20437",
|
||||
"sdk_s_route": "/api/sworder-server/rule/v1/getUserRisk",
|
||||
"sdk_sword_holder_result": "999999",
|
||||
"proj_cloud_id": "3",
|
||||
"proj_app_id": "10010",
|
||||
"ip_address": "59.58.58.18",
|
||||
"sdk_sword_holder_type": "login",
|
||||
"sdk_sword_holder_name": "非信任设备禁止登录",
|
||||
"proj_client_id": "Android_5.505_tyGuest,tyAccount,yidunlogin.weixinPay,alipay,yinlian,jingdong,weixinShare.0-hall28.official.fish3d",
|
||||
"sdk_sword_holder_process": "{\"330\":[\"[330-0]未命中\"]}",
|
||||
"sdk_yidun_device_id": "UuCJztYTtxRETUVUAEaFoCQaw8H2qkWE",
|
||||
"platform_id": "1",
|
||||
"sub_channel_id": "sdkonline",
|
||||
"proj_package_name": "",
|
||||
"channel_id": "tuyoo"
|
||||
},
|
||||
"lib": {
|
||||
"lib_version": "v1.0.0",
|
||||
"lib_type": "go"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## asset日志格式范例
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "28",
|
||||
"type": "asset",
|
||||
"event": "asset_increase",
|
||||
"event_time": 1706631881042,
|
||||
"device_id": "",
|
||||
"user_id": "928426614",
|
||||
"client_id": "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz",
|
||||
"properties": {
|
||||
"proj_ga_eventId": "STARTUP_QUEST_REWARD_COIN",
|
||||
"proj_asset_value": "2",
|
||||
"proj_asset_final": "2",
|
||||
"proj_chip_type": "6",
|
||||
"proj_asset_id": "13101",
|
||||
"proj_asset_name": "\u9501\u5b9a",
|
||||
"proj_asset_type": "free",
|
||||
"proj_asset_time_limit": "0",
|
||||
"proj_asset_start_time": "0",
|
||||
"proj_asset_source": "",
|
||||
"proj_tuyoo_order_id": "",
|
||||
"game_id": "28",
|
||||
"app_id": "10063",
|
||||
"proj_project_id": "28",
|
||||
"proj_client_id": "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz",
|
||||
"uuid": "77b907688c624076a84ca7b4b29668a5"
|
||||
},
|
||||
"lib": {
|
||||
"lib_version": "v1.0.0",
|
||||
"lib_type": "go"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
| 版本 | 修订说明 | 提交人 | 发布时间 |
|
||||
|----- | ----- | ----- | ----- |
|
||||
| v1.0.0 | galog 0依赖、支持异步写入、小时切割日志、ga和asset日志类型 | 田文 | 2024.02.22 |
|
||||
| v1.1.0 | add suger | 田文 | 2025.06.30 |
|
||||
@ -1,78 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultBufPool = newBufPool()
|
||||
)
|
||||
|
||||
const (
|
||||
_size = 1024
|
||||
_defaultLineEnding = "\n"
|
||||
)
|
||||
|
||||
type BufPool struct {
|
||||
p *sync.Pool
|
||||
}
|
||||
|
||||
func newBufPool() BufPool {
|
||||
return BufPool{p: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &Buffer{bs: make([]byte, 0, _size)}
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// Get retrieves a Buffer from the pool, creating one if necessary.
|
||||
func (p BufPool) Get() *Buffer {
|
||||
buf := p.p.Get().(*Buffer)
|
||||
buf.Reset()
|
||||
buf.pool = p
|
||||
return buf
|
||||
}
|
||||
|
||||
func (p BufPool) put(buf *Buffer) {
|
||||
p.p.Put(buf)
|
||||
}
|
||||
|
||||
// Buffer is a thin wrapper around a byte slice. It's intended to be pooled, so
|
||||
// the only way to construct one is via a Pool.
|
||||
type Buffer struct {
|
||||
bs []byte
|
||||
pool BufPool
|
||||
}
|
||||
|
||||
// AppendByte writes a single byte to the Buffer.
|
||||
func (b *Buffer) AppendByte(v byte) {
|
||||
b.bs = append(b.bs, v)
|
||||
}
|
||||
|
||||
// AppendString writes a string to the Buffer.
|
||||
func (b *Buffer) AppendString(s string) {
|
||||
b.bs = append(b.bs, s...)
|
||||
}
|
||||
|
||||
// Bytes returns a mutable reference to the underlying byte slice.
|
||||
func (b *Buffer) Bytes() []byte {
|
||||
return b.bs
|
||||
}
|
||||
|
||||
// String returns a string copy of the underlying byte slice.
|
||||
func (b *Buffer) String() string {
|
||||
return string(b.bs)
|
||||
}
|
||||
|
||||
// Reset resets the underlying byte slice. Subsequent writes re-use the slice's
|
||||
// backing array.
|
||||
func (b *Buffer) Reset() {
|
||||
b.bs = b.bs[:0]
|
||||
}
|
||||
|
||||
// Free returns the Buffer to its Pool.
|
||||
//
|
||||
// Callers must not retain references to the Buffer after calling Free.
|
||||
func (b *Buffer) Free() {
|
||||
b.pool.put(b)
|
||||
}
|
||||
@ -1,139 +0,0 @@
|
||||
package galog
|
||||
|
||||
var (
|
||||
defaultLib = map[string]string{"lib_type": "go", "lib_version": "v1.0.0"}
|
||||
)
|
||||
|
||||
type EntryBean struct {
|
||||
// ProjectID 独立项目ID
|
||||
ProjectID string `json:"project_id"`
|
||||
// LogType 日志类型 默认是track
|
||||
LogType LogType `json:"type"`
|
||||
// Event 事件ID
|
||||
Event string `json:"event"`
|
||||
// EventTime 毫秒级时间
|
||||
EventTime int64 `json:"event_time"`
|
||||
// DeviceID 设备ID
|
||||
DeviceID string `json:"device_id"`
|
||||
// UserID 用户ID
|
||||
UserID string `json:"user_id"`
|
||||
// ClientID clientID
|
||||
ClientID string `json:"client_id"`
|
||||
// Properties 重要额外字段
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
// Lib 代码库
|
||||
Lib map[string]string `json:"lib"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
logger *GALogger
|
||||
bean *EntryBean
|
||||
}
|
||||
|
||||
func (e *Entry) SetUserID(uid string) *Entry {
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
e.bean.UserID = uid
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Entry) SetDeviceID(did string) *Entry {
|
||||
e.bean.DeviceID = did
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Entry) SetLib(lib map[string]string) *Entry {
|
||||
e.bean.Lib = lib
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Entry) SetClientID(cid string) *Entry {
|
||||
e.bean.ClientID = cid
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Entry) SetProperties(props map[string]interface{}) *Entry {
|
||||
e.bean.Properties = props
|
||||
return e
|
||||
}
|
||||
|
||||
// Flush 日志写入
|
||||
func (e *Entry) Flush() {
|
||||
defer e.logger.putEntry(e)
|
||||
e.logger.biz(e.bean)
|
||||
}
|
||||
|
||||
type AssetProperties map[string]interface{}
|
||||
|
||||
// SetAssetID 资产id
|
||||
func (ap AssetProperties) SetAssetID(assetID string) AssetProperties {
|
||||
ap["proj_asset_id"] = assetID
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetName 资产名称
|
||||
func (ap AssetProperties) SetAssetName(assetName string) AssetProperties {
|
||||
ap["proj_asset_name"] = assetName
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetType 资产付费类型
|
||||
func (ap AssetProperties) SetAssetType(assetType string) AssetProperties {
|
||||
ap["proj_asset_type"] = assetType
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetValue 资产变化数量
|
||||
func (ap AssetProperties) SetAssetValue(assetValue string) AssetProperties {
|
||||
ap["proj_asset_value"] = assetValue
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetFinal 资产变化后的剩余数量
|
||||
func (ap AssetProperties) SetAssetFinal(assetFinal string) AssetProperties {
|
||||
ap["proj_asset_final"] = assetFinal
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetTimeLimit 持续性资产持续时间
|
||||
func (ap AssetProperties) SetAssetTimeLimit(assetTimeLimit string) AssetProperties {
|
||||
ap["proj_asset_time_limit"] = assetTimeLimit
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetStartTime 持续性资产生效时间
|
||||
func (ap AssetProperties) SetAssetStartTime(assetStartTime string) AssetProperties {
|
||||
ap["proj_asset_start_time"] = assetStartTime
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetSource 变化的原因
|
||||
func (ap AssetProperties) SetAssetSource(assetSource string) AssetProperties {
|
||||
ap["proj_asset_source"] = assetSource
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetAssetAssociated 变化关联资产
|
||||
func (ap AssetProperties) SetAssetAssociated(assetAssociated string) AssetProperties {
|
||||
ap["proj_asset_associated"] = assetAssociated
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetTuyooOrderID SDK订单号
|
||||
func (ap AssetProperties) SetTuyooOrderID(tuyooOrderID string) AssetProperties {
|
||||
ap["proj_tuyoo_order_id"] = tuyooOrderID
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetOrderID 游戏服订单号
|
||||
func (ap AssetProperties) SetOrderID(assetOrderID string) AssetProperties {
|
||||
ap["proj_order_id"] = assetOrderID
|
||||
return ap
|
||||
}
|
||||
|
||||
// SetKV 设置自定义字段必须通知到数据组,否则打点失败
|
||||
func (ap AssetProperties) SetKV(k, v string) AssetProperties {
|
||||
ap[k] = v
|
||||
return ap
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
|
||||
"tygit.tuyoo.com/gocomponents/galog/examples/worker"
|
||||
)
|
||||
|
||||
var (
|
||||
output = flag.String("o", "", "")
|
||||
|
||||
c = flag.Int("c", 50, "")
|
||||
n = flag.Int("n", 200, "")
|
||||
q = flag.Float64("q", 0, "")
|
||||
|
||||
cpus = flag.Int("cpus", runtime.GOMAXPROCS(-1), "")
|
||||
)
|
||||
|
||||
var usage = `Usage: hey [options...] <url>
|
||||
|
||||
Options:
|
||||
-n Number of requests to run. Default is 200.
|
||||
-c Number of workers to run concurrently. Total number of requests cannot
|
||||
be smaller than the concurrency level. Default is 50.
|
||||
-q Rate limit, in queries per second (QPS) per worker. Default is no rate limit.
|
||||
-o Output type. If none provided, a summary is printed.
|
||||
"csv" is the only supported alternative. Dumps the response
|
||||
metrics in comma-separated values format.
|
||||
|
||||
-cpus Number of used cpu cores.
|
||||
(default for current machine is %d cores)
|
||||
`
|
||||
|
||||
func MainHey() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, usage, runtime.NumCPU())
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
runtime.GOMAXPROCS(*cpus)
|
||||
num := *n
|
||||
conc := *c
|
||||
q := *q
|
||||
|
||||
if num <= 0 || conc <= 0 {
|
||||
usageAndExit("-n and -c cannot be smaller than 1.")
|
||||
}
|
||||
|
||||
if num < conc {
|
||||
usageAndExit("-n cannot be less than -c.")
|
||||
}
|
||||
|
||||
w := &worker.Work{
|
||||
N: num,
|
||||
C: conc,
|
||||
QPS: q,
|
||||
Output: *output,
|
||||
}
|
||||
w.Init()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
w.Stop()
|
||||
}()
|
||||
w.Run()
|
||||
}
|
||||
|
||||
func usageAndExit(msg string) {
|
||||
if msg != "" {
|
||||
fmt.Fprint(os.Stderr, msg)
|
||||
fmt.Fprintf(os.Stderr, "\n\n")
|
||||
}
|
||||
flag.Usage()
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"tygit.tuyoo.com/gocomponents/galog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("log ga asset server every 5 seconds.")
|
||||
go logGA()
|
||||
go logAsset()
|
||||
logServer()
|
||||
}
|
||||
|
||||
func logGA() {
|
||||
projectID := "20433"
|
||||
clientID := "Android_5.0_tyGuest,weixinPay,tyAccount.alipay.0-hall20433.tuyoo.sdktest"
|
||||
logger, err := galog.NewServerGALogger(&galog.ServerGALogOptions{
|
||||
ProjectID: projectID,
|
||||
ClientID: clientID,
|
||||
LogDir: "ga_log",
|
||||
LogType: galog.LogTypeTrack,
|
||||
EnableAsync: true,
|
||||
AsyncQueueSize: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
t := time.NewTicker(5 * time.Second)
|
||||
|
||||
for tt := range t.C {
|
||||
log.Println("logGA")
|
||||
logger.
|
||||
GetEntry("sdk_s_login_succ").
|
||||
SetDeviceID("device001").
|
||||
SetUserID("10086").
|
||||
SetProperties(map[string]interface{}{
|
||||
"ip_address": "127.0.0.1",
|
||||
"proj_app_id": "10010",
|
||||
"uuid": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts": tt.UnixNano(),
|
||||
}).
|
||||
Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func logAsset() {
|
||||
projectID := "28"
|
||||
clientID := "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz"
|
||||
logger, err := galog.NewGALogger(".", projectID, clientID, galog.LogTypeAsset)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
t := time.NewTicker(5 * time.Second)
|
||||
|
||||
asset := galog.AssetProperties{}
|
||||
asset.SetAssetID("13101").
|
||||
SetAssetName("\u9501\u5b9a").
|
||||
SetAssetType("6").
|
||||
SetAssetFinal("2").
|
||||
SetAssetAssociated("3").
|
||||
SetAssetStartTime("0").
|
||||
SetAssetTimeLimit("0").
|
||||
SetAssetSource("").
|
||||
SetKV("uuid", "uuid-v4")
|
||||
|
||||
for tt := range t.C {
|
||||
log.Println("logAssert")
|
||||
_ = tt
|
||||
logger.
|
||||
GetEntry("asset_increase").
|
||||
SetDeviceID("").
|
||||
SetUserID("10086").
|
||||
SetProperties(asset).
|
||||
Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func logServer() {
|
||||
slogger, _ := galog.NewServerLogger(&galog.ServerLogOptions{
|
||||
LogDir: "logs",
|
||||
EnableAsync: true,
|
||||
AsyncQueueSize: 1000,
|
||||
})
|
||||
|
||||
t := time.NewTicker(5 * time.Second)
|
||||
|
||||
for tt := range t.C {
|
||||
log.Println("logServer")
|
||||
_ = tt
|
||||
|
||||
msgMap := map[string]interface{}{
|
||||
"CreateTime": time.Now().UnixNano() / int64(time.Microsecond),
|
||||
"Host": "host",
|
||||
"AppId": "10010",
|
||||
"UserId": "12345",
|
||||
"Level": "Notice",
|
||||
"Entry": "Login",
|
||||
"Func": "HandleLogin",
|
||||
"TraceMsg": "HandlerWSFriApplyList|HandlerWSFriApplyList",
|
||||
"Params": "{\"isoCode\":\"CN\",\"P0\":[3,\"Total: 3, End: 0, \"],\"pf\":\"wx\",\"appVer\":\"1.0\",\"sdkVer\":\"1.0\",\"ip\":\"58.247.195.158\",\"clientId\":\"7abd64bf-3fb6-4fef-a25e-e6562b7fb857\",\"timeZone\":\"Asia/Shanghai\",\"loginMark\":\"\",\"st\":1472}",
|
||||
}
|
||||
slogger.BizErr(msgMap)
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func newTemplate(output string) *template.Template {
|
||||
outputTmpl := output
|
||||
switch outputTmpl {
|
||||
case "":
|
||||
outputTmpl = defaultTmpl
|
||||
case "csv":
|
||||
outputTmpl = csvTmpl
|
||||
}
|
||||
return template.Must(template.New("tmpl").Funcs(tmplFuncMap).Parse(outputTmpl))
|
||||
}
|
||||
|
||||
var tmplFuncMap = template.FuncMap{
|
||||
"formatNumber": formatNumber,
|
||||
"formatNumberInt": formatNumberInt,
|
||||
}
|
||||
|
||||
func formatNumber(duration float64) string {
|
||||
return fmt.Sprintf("%4.4f", duration)
|
||||
}
|
||||
|
||||
func formatNumberInt(duration int) string {
|
||||
return fmt.Sprintf("%d", duration)
|
||||
}
|
||||
|
||||
var (
|
||||
defaultTmpl = `
|
||||
Summary:
|
||||
Total: {{ formatNumber .Total.Seconds }} secs
|
||||
Requests/sec: {{ formatNumber .Rps }}
|
||||
|
||||
TotalNumRes: {{ .NumRes }}
|
||||
AvgTotal: {{ .AvgTotal }}
|
||||
`
|
||||
csvTmpl = `{{ $connLats := .ConnLats }}{{ $dnsLats := .DnsLats }}{{ $dnsLats := .DnsLats }}{{ $reqLats := .ReqLats }}{{ $delayLats := .DelayLats }}{{ $resLats := .ResLats }}{{ $statusCodeLats := .StatusCodes }}{{ $offsets := .Offsets}}response-time,DNS+dialup,DNS,Request-write,Response-delay,Response-read,status-code,offset{{ range $i, $v := .Lats }}
|
||||
{{ formatNumber $v }},{{ formatNumber (index $connLats $i) }},{{ formatNumber (index $dnsLats $i) }},{{ formatNumber (index $reqLats $i) }},{{ formatNumber (index $delayLats $i) }},{{ formatNumber (index $resLats $i) }},{{ formatNumberInt (index $statusCodeLats $i) }},{{ formatNumber (index $offsets $i) }}{{ end }}`
|
||||
)
|
||||
@ -1,94 +0,0 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type report struct {
|
||||
avgTotal float64
|
||||
rps float64
|
||||
|
||||
results chan *result
|
||||
done chan bool
|
||||
total time.Duration
|
||||
|
||||
numRes int64
|
||||
output string
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newReport(w io.Writer, results chan *result, output string, n int) *report {
|
||||
return &report{
|
||||
output: output,
|
||||
results: results,
|
||||
done: make(chan bool, 1),
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
func runReporter(r *report) {
|
||||
// Loop will continue until channel is closed
|
||||
for res := range r.results {
|
||||
r.numRes++
|
||||
r.avgTotal += res.duration.Seconds()
|
||||
}
|
||||
// Signal reporter is done.
|
||||
r.done <- true
|
||||
}
|
||||
|
||||
func (r *report) finalize(total time.Duration) {
|
||||
r.total = total
|
||||
r.rps = float64(r.numRes) / r.total.Seconds()
|
||||
r.print()
|
||||
}
|
||||
|
||||
func (r *report) print() {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := newTemplate(r.output).Execute(buf, r.snapshot()); err != nil {
|
||||
log.Println("error:", err.Error())
|
||||
return
|
||||
}
|
||||
r.printf(buf.String())
|
||||
|
||||
r.printf("\n")
|
||||
}
|
||||
|
||||
func (r *report) printf(s string, v ...interface{}) {
|
||||
fmt.Fprintf(r.w, s, v...)
|
||||
}
|
||||
|
||||
func (r *report) snapshot() Report {
|
||||
snapshot := Report{
|
||||
AvgTotal: r.avgTotal,
|
||||
Rps: r.rps,
|
||||
Total: r.total,
|
||||
NumRes: r.numRes,
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
AvgTotal float64
|
||||
Rps float64
|
||||
|
||||
Total time.Duration
|
||||
|
||||
NumRes int64
|
||||
}
|
||||
|
||||
type LatencyDistribution struct {
|
||||
Percentage int
|
||||
Latency float64
|
||||
}
|
||||
|
||||
type Bucket struct {
|
||||
Mark float64
|
||||
Count int
|
||||
Frequency float64
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tygit.tuyoo.com/gocomponents/galog"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
// now returns time.Duration using stdlib time
|
||||
func now() time.Duration { return time.Since(startTime) }
|
||||
|
||||
// Max size of the buffer of result channel.
|
||||
const maxResult = 1000000
|
||||
|
||||
type result struct {
|
||||
offset time.Duration
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
type Work struct {
|
||||
// N is the total number of requests to make.
|
||||
N int
|
||||
|
||||
// C is the concurrency level, the number of concurrent workers to run.
|
||||
C int
|
||||
|
||||
// Qps is the rate limit in queries per second.
|
||||
QPS float64
|
||||
|
||||
// Output represents the output type. If "csv" is provided, the
|
||||
// output will be dumped as a csv stream.
|
||||
Output string
|
||||
|
||||
// Writer is where results will be written. If nil, results are written to stdout.
|
||||
Writer io.Writer
|
||||
|
||||
initOnce sync.Once
|
||||
results chan *result
|
||||
stopCh chan struct{}
|
||||
start time.Duration
|
||||
|
||||
report *report
|
||||
}
|
||||
|
||||
func (b *Work) writer() io.Writer {
|
||||
if b.Writer == nil {
|
||||
return os.Stdout
|
||||
}
|
||||
return b.Writer
|
||||
}
|
||||
|
||||
// Init initializes internal data-structures
|
||||
func (b *Work) Init() {
|
||||
b.initOnce.Do(func() {
|
||||
b.results = make(chan *result, min(b.C*1000, maxResult))
|
||||
b.stopCh = make(chan struct{}, b.C)
|
||||
b.initLogger()
|
||||
})
|
||||
}
|
||||
|
||||
// Run makes all the requests, prints the summary. It blocks until
|
||||
// all work is done.
|
||||
func (b *Work) Run() {
|
||||
b.Init()
|
||||
b.start = now()
|
||||
b.report = newReport(b.writer(), b.results, b.Output, b.N)
|
||||
// Run the reporter first, it polls the result channel until it is closed.
|
||||
go func() {
|
||||
runReporter(b.report)
|
||||
}()
|
||||
b.runWorkers()
|
||||
b.Finish()
|
||||
}
|
||||
|
||||
func (b *Work) Stop() {
|
||||
// Send stop signal so that workers can stop gracefully.
|
||||
for i := 0; i < b.C; i++ {
|
||||
b.stopCh <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Work) Finish() {
|
||||
close(b.results)
|
||||
total := now() - b.start
|
||||
// Wait until the reporter is done.
|
||||
<-b.report.done
|
||||
b.report.finalize(total)
|
||||
}
|
||||
|
||||
var logger *galog.GALogger
|
||||
|
||||
func (b *Work) initLogger() {
|
||||
projectID := "28"
|
||||
clientID := "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz"
|
||||
logger, _ = galog.NewGALogger("logs", projectID, clientID, galog.LogTypeAsset)
|
||||
}
|
||||
|
||||
func (b *Work) doSth() {
|
||||
s := now()
|
||||
|
||||
// doSth here.
|
||||
asset := galog.AssetProperties{}
|
||||
asset.SetAssetID("13101").
|
||||
SetAssetName("\u9501\u5b9a").
|
||||
SetAssetType("6").
|
||||
SetAssetFinal("2").
|
||||
SetAssetAssociated("3").
|
||||
SetAssetStartTime("0").
|
||||
SetAssetTimeLimit("0").
|
||||
SetAssetSource("").
|
||||
SetKV("uuid", strconv.Itoa(int(time.Now().UnixNano())))
|
||||
logger.
|
||||
GetEntry("asset_increase").
|
||||
SetDeviceID("").
|
||||
SetUserID("10086").
|
||||
SetProperties(asset).
|
||||
Flush()
|
||||
|
||||
t := now()
|
||||
finish := t - s
|
||||
b.results <- &result{
|
||||
offset: s,
|
||||
duration: finish,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Work) runWorker(n int) {
|
||||
var throttle <-chan time.Time
|
||||
if b.QPS > 0 {
|
||||
t := time.NewTicker(time.Duration(1e6/(b.QPS)) * time.Microsecond)
|
||||
throttle = t.C
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
// Check if application is stopped. Do not send into a closed channel.
|
||||
select {
|
||||
case <-b.stopCh:
|
||||
return
|
||||
default:
|
||||
if b.QPS > 0 {
|
||||
<-throttle
|
||||
}
|
||||
b.doSth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Work) runWorkers() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(b.C)
|
||||
|
||||
fmt.Printf("==RUN worker==> N:%d C:%d QPS:%v b.N / b.C:%v", b.N, b.C, b.QPS, b.N/b.C)
|
||||
|
||||
// Ignore the case where b.N % b.C != 0.
|
||||
for i := 0; i < b.C; i++ {
|
||||
go func() {
|
||||
b.runWorker(b.N / b.C)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@ -1,264 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fileLogWriter struct {
|
||||
sync.RWMutex
|
||||
|
||||
Rotate bool `json:"rotate"`
|
||||
Hourly bool `json:"hourly"`
|
||||
|
||||
// The opened file
|
||||
Filename string `json:"filename"`
|
||||
fileWriter *os.File
|
||||
|
||||
// Rotate hourly
|
||||
MaxHours int64 `json:"maxhours"` // 默认不删除日志
|
||||
hourlyOpenDate int
|
||||
hourlyOpenTime time.Time
|
||||
|
||||
// Permissions for log file
|
||||
Perm string `json:"perm"`
|
||||
// Permissions for directory if it is specified in FileName
|
||||
DirPerm string `json:"dirperm"`
|
||||
|
||||
RotatePerm string `json:"rotateperm"`
|
||||
|
||||
fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix
|
||||
}
|
||||
|
||||
func newFileWriter() LogProvider {
|
||||
w := &fileLogWriter{
|
||||
Rotate: true, // 开启日志轮转
|
||||
Hourly: true, // 开启日志小时轮转
|
||||
RotatePerm: "0440",
|
||||
Perm: "0660",
|
||||
DirPerm: "0770",
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Init file logger with json config.
|
||||
// jsonConfig like:
|
||||
//
|
||||
// {"filename":"logs/glog.log"}
|
||||
func (w *fileLogWriter) Init(config string) error {
|
||||
err := json.Unmarshal([]byte(config), w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Filename == "" {
|
||||
return errors.New("jsonconfig must have filename")
|
||||
}
|
||||
w.suffix = filepath.Ext(w.Filename)
|
||||
w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix)
|
||||
if w.suffix == "" {
|
||||
w.suffix = ".log"
|
||||
}
|
||||
|
||||
err = w.startLogger()
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) startLogger() error {
|
||||
file, err := w.createLogFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.fileWriter != nil {
|
||||
w.fileWriter.Close()
|
||||
}
|
||||
w.fileWriter = file
|
||||
return w.initFd()
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) needRotateHourly(hour int) bool {
|
||||
return w.Hourly && hour != w.hourlyOpenDate
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) createLogFile() (*os.File, error) {
|
||||
perm, err := strconv.ParseInt(w.Perm, 8, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dirperm, err := strconv.ParseInt(w.DirPerm, 8, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filepath := path.Dir(w.Filename)
|
||||
os.MkdirAll(filepath, os.FileMode(dirperm))
|
||||
|
||||
fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
|
||||
if err == nil {
|
||||
os.Chmod(w.Filename, os.FileMode(perm))
|
||||
}
|
||||
return fd, err
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) initFd() error {
|
||||
w.hourlyOpenTime = time.Now()
|
||||
w.hourlyOpenDate = w.hourlyOpenTime.Hour()
|
||||
if w.Hourly {
|
||||
go w.hourlyRotate(w.hourlyOpenTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) hourlyRotate(openTime time.Time) {
|
||||
y, m, d := openTime.Add(1 * time.Hour).Date()
|
||||
h := openTime.Add(1 * time.Hour).Hour()
|
||||
nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location())
|
||||
tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100))
|
||||
<-tm.C
|
||||
w.Lock()
|
||||
if w.needRotateHourly(time.Now().Hour()) {
|
||||
if err := w.doRotate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
||||
}
|
||||
}
|
||||
w.Unlock()
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) doRotate() error {
|
||||
fName := ""
|
||||
format := ""
|
||||
var openTime time.Time
|
||||
rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = os.Lstat(w.Filename)
|
||||
if err != nil {
|
||||
goto RESTART_LOGGER
|
||||
}
|
||||
|
||||
if w.Hourly {
|
||||
format = "20060102_15"
|
||||
openTime = w.hourlyOpenTime
|
||||
}
|
||||
|
||||
fName = w.fileNameOnly + fmt.Sprintf("_%s%s", openTime.Format(format), w.suffix)
|
||||
_, err = os.Lstat(fName)
|
||||
|
||||
// return error if the last file checked still existed
|
||||
if err == nil {
|
||||
//return fmt.Errorf("rotate: cannot find free log number to rename %s", w.Filename)
|
||||
}
|
||||
|
||||
w.fileWriter.Close()
|
||||
|
||||
// Rename the file to its new found name
|
||||
// even if occurs error,we MUST guarantee to restart new logger
|
||||
err = os.Rename(w.Filename, fName)
|
||||
if err != nil {
|
||||
goto RESTART_LOGGER
|
||||
}
|
||||
|
||||
err = os.Chmod(fName, os.FileMode(rotatePerm))
|
||||
|
||||
RESTART_LOGGER:
|
||||
|
||||
startLoggerErr := w.startLogger()
|
||||
if w.MaxHours > 0 {
|
||||
go w.deleteOldLog()
|
||||
}
|
||||
|
||||
if startLoggerErr != nil {
|
||||
return fmt.Errorf("rotate startLogger: %s", startLoggerErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) deleteOldLog() {
|
||||
dir := filepath.Dir(w.Filename)
|
||||
absolutePath, err := filepath.EvalSymlinks(w.Filename)
|
||||
if err == nil {
|
||||
dir = filepath.Dir(absolutePath)
|
||||
}
|
||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r)
|
||||
}
|
||||
}()
|
||||
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
if w.Hourly {
|
||||
if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) {
|
||||
if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
|
||||
strings.HasSuffix(filepath.Base(path), w.suffix) {
|
||||
os.Remove(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// EncodeMsg encode log msg
|
||||
func (*fileLogWriter) EncodeMsg(lm *LogMsg) []byte {
|
||||
buf := defaultBufPool.Get()
|
||||
buf.AppendString(lm.Msg)
|
||||
buf.AppendString(_defaultLineEnding)
|
||||
msg := buf.Bytes()
|
||||
buf.Free()
|
||||
return msg
|
||||
}
|
||||
|
||||
// WriteMsg write msg to log and rotate
|
||||
func (w *fileLogWriter) WriteMsg(lm *LogMsg) error {
|
||||
h := lm.When.Hour()
|
||||
|
||||
if w.Rotate {
|
||||
w.RLock()
|
||||
if w.needRotateHourly(h) {
|
||||
w.RUnlock()
|
||||
w.Lock()
|
||||
if w.needRotateHourly(h) {
|
||||
if err := w.doRotate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
||||
}
|
||||
}
|
||||
w.Unlock()
|
||||
} else {
|
||||
w.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
msg := w.EncodeMsg(lm)
|
||||
_, err := w.fileWriter.Write(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// Destroy close the file description, close file writer.
|
||||
func (w *fileLogWriter) Destroy() {
|
||||
w.fileWriter.Close()
|
||||
}
|
||||
|
||||
// Flush flushes file logger.
|
||||
func (w *fileLogWriter) Flush() {
|
||||
w.fileWriter.Sync()
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterFile, newFileWriter)
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GALogger struct {
|
||||
logger *Logger
|
||||
projectID string
|
||||
clientID string
|
||||
logType LogType
|
||||
entryPool sync.Pool
|
||||
}
|
||||
|
||||
type LogType string
|
||||
|
||||
const (
|
||||
// LogTypeTrack 事实日志
|
||||
LogTypeTrack LogType = "track"
|
||||
// LogTypeAsset 资产日志
|
||||
LogTypeAsset LogType = "asset"
|
||||
)
|
||||
|
||||
const (
|
||||
// enableAsync 启用异步写入模式
|
||||
enableAsync = true
|
||||
// defaultAsyncMsgLen 异步写入模式的队列长度,QPS较大时可调整此值
|
||||
defaultAsyncMsgLen int64 = 100
|
||||
)
|
||||
|
||||
// NewGALogger
|
||||
//
|
||||
// logDir: 日志根目录 logs | /home/tywork
|
||||
// projectID: projectID
|
||||
// clientID: clientID
|
||||
// logType: galog.LogTypeTrack | galog.LogTypeAsset
|
||||
func NewGALogger(logDir, projectID, clientID string, logType LogType) (*GALogger, error) {
|
||||
l := newLogger()
|
||||
hostname, _ := os.Hostname()
|
||||
filepath := fmt.Sprintf("ga_log/ga_%s.log", hostname)
|
||||
if logType == LogTypeAsset {
|
||||
filepath = fmt.Sprintf("asset_log/%s_%s.log", logType, hostname)
|
||||
}
|
||||
configs := fmt.Sprintf(`{"filename":"%s/%s"}`, strings.TrimRight(logDir, "/"), filepath)
|
||||
err := l.setLogger(AdapterFile, configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if enableAsync {
|
||||
l.Async()
|
||||
}
|
||||
gaLogger := &GALogger{
|
||||
logger: l,
|
||||
projectID: projectID,
|
||||
clientID: clientID,
|
||||
logType: logType,
|
||||
}
|
||||
|
||||
return gaLogger, nil
|
||||
}
|
||||
|
||||
// GetEntry
|
||||
func (gl *GALogger) GetEntry(event string) *Entry {
|
||||
e, ok := gl.entryPool.Get().(*Entry)
|
||||
if ok {
|
||||
e.logger = gl
|
||||
e.bean.ProjectID = gl.projectID
|
||||
e.bean.LogType = gl.logType
|
||||
e.bean.ClientID = gl.clientID
|
||||
e.bean.Event = event
|
||||
e.bean.EventTime = time.Now().UnixMilli()
|
||||
e.bean.Lib = defaultLib
|
||||
return e
|
||||
}
|
||||
return &Entry{
|
||||
logger: gl,
|
||||
bean: &EntryBean{
|
||||
ProjectID: gl.projectID,
|
||||
LogType: gl.logType,
|
||||
ClientID: gl.clientID,
|
||||
Event: event,
|
||||
EventTime: time.Now().UnixMilli(),
|
||||
Lib: defaultLib,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gl *GALogger) biz(v interface{}) {
|
||||
gl.logger.BizErr(v)
|
||||
}
|
||||
|
||||
func (gl *GALogger) putEntry(entry *Entry) {
|
||||
entry.bean = &EntryBean{}
|
||||
gl.entryPool.Put(entry)
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
gaLogger *GALogger
|
||||
assetLogger *GALogger
|
||||
)
|
||||
|
||||
// go test -bench . -benchmem
|
||||
func init() {
|
||||
gaLogger, _ = NewGALogger("logs", "20433", "Android_5.0_tyGuest,weixinPay,tyAccount.alipay.0-hall20433.tuyoo.sdktest", LogTypeTrack)
|
||||
assetLogger, _ = NewGALogger("logs", "28", "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz", LogTypeAsset)
|
||||
}
|
||||
|
||||
func BenchmarkGaLog(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
props := map[string]interface{}{
|
||||
"ip_address": "127.0.0.1",
|
||||
"proj_app_id": "10010",
|
||||
"uuid": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts": time.Now().UnixNano(),
|
||||
"ip_address1": "127.0.0.1",
|
||||
"proj_app_id1": "10010",
|
||||
"uuid1": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts1": time.Now().UnixNano(),
|
||||
"ip_address2": "127.0.0.1",
|
||||
"proj_app_id2": "10010",
|
||||
"uuid2": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts2": time.Now().UnixNano(),
|
||||
"ip_address3": "127.0.0.1",
|
||||
"proj_app_id3": "10010",
|
||||
"uuid3": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts4": time.Now().UnixNano(),
|
||||
"proj_app_id25": "10010",
|
||||
"uuid21": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts21": time.Now().UnixNano(),
|
||||
"ip_address31": "127.0.0.1",
|
||||
"proj_app_id31": "10010",
|
||||
"uuid31": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts11": time.Now().UnixNano(),
|
||||
"uuid3111": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts4111": time.Now().UnixNano(),
|
||||
"proj_app_id21115": "10010",
|
||||
"uuid111": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts111": time.Now().UnixNano(),
|
||||
"ip_a11ddress31": "127.0.0.1",
|
||||
"proj1_app_id31": "10010",
|
||||
"uuid131": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67",
|
||||
"ts1111": time.Now().UnixNano(),
|
||||
}
|
||||
gaLogger.
|
||||
GetEntry("sdk_s_login_succ").
|
||||
SetDeviceID("device001").
|
||||
SetUserID("10086").
|
||||
SetProperties(props).
|
||||
Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAssetLog(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
asset := make(AssetProperties)
|
||||
asset.
|
||||
SetAssetID("13101").
|
||||
SetAssetName("\u9501\u5b9a").
|
||||
SetAssetType("6").
|
||||
SetAssetFinal("2").
|
||||
SetAssetAssociated("3").
|
||||
SetAssetStartTime("0").
|
||||
SetAssetTimeLimit("0").
|
||||
SetAssetSource("").
|
||||
SetKV("uuid", "uuid-v4")
|
||||
|
||||
assetLogger.
|
||||
GetEntry("asset_increase").
|
||||
SetDeviceID("").
|
||||
SetUserID("10086").
|
||||
SetProperties(asset).
|
||||
Flush()
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
module tygit.tuyoo.com/gocomponents/galog
|
||||
|
||||
go 1.20
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 234 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 216 KiB |
@ -1,261 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
AdapterFile = "file"
|
||||
)
|
||||
|
||||
type LogMsg struct {
|
||||
Msg string
|
||||
When time.Time
|
||||
}
|
||||
|
||||
// Logger defines the behavior of a log provider.
|
||||
type LogProvider interface {
|
||||
Init(config string) error
|
||||
WriteMsg(lm *LogMsg) error
|
||||
Destroy()
|
||||
Flush()
|
||||
}
|
||||
|
||||
type newLogProviderFunc func() LogProvider
|
||||
|
||||
var (
|
||||
adapters = make(map[string]newLogProviderFunc)
|
||||
)
|
||||
|
||||
// Register register a new log provider.
|
||||
func Register(name string, log newLogProviderFunc) {
|
||||
if log == nil {
|
||||
panic("logs: Register provide is nil")
|
||||
}
|
||||
if _, dup := adapters[name]; dup {
|
||||
panic("logs: Register called twice for provider " + name)
|
||||
}
|
||||
adapters[name] = log
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
lock sync.Mutex
|
||||
asynchronous bool
|
||||
wg sync.WaitGroup
|
||||
msgChanLen int64
|
||||
msgChan chan *LogMsg
|
||||
closeChan chan struct{}
|
||||
flushChan chan struct{}
|
||||
outputs []*nameLogger
|
||||
}
|
||||
|
||||
type nameLogger struct {
|
||||
LogProvider
|
||||
name string
|
||||
}
|
||||
|
||||
var logMsgPool *sync.Pool
|
||||
|
||||
// newLogger return a new Logger.
|
||||
func newLogger(channelLens ...int64) *Logger {
|
||||
gl := new(Logger)
|
||||
gl.msgChanLen = append(channelLens, 0)[0]
|
||||
if gl.msgChanLen <= 0 {
|
||||
gl.msgChanLen = defaultAsyncMsgLen
|
||||
}
|
||||
gl.flushChan = make(chan struct{}, 1)
|
||||
gl.closeChan = make(chan struct{}, 1)
|
||||
return gl
|
||||
}
|
||||
|
||||
// Async sets the log to asynchronous and start the goroutine
|
||||
func (gl *Logger) Async(msgLen ...int64) *Logger {
|
||||
gl.lock.Lock()
|
||||
defer gl.lock.Unlock()
|
||||
if gl.asynchronous {
|
||||
return gl
|
||||
}
|
||||
gl.asynchronous = true
|
||||
if len(msgLen) > 0 && msgLen[0] > 0 {
|
||||
gl.msgChanLen = msgLen[0]
|
||||
}
|
||||
gl.msgChan = make(chan *LogMsg, gl.msgChanLen)
|
||||
logMsgPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &LogMsg{}
|
||||
},
|
||||
}
|
||||
gl.wg.Add(1)
|
||||
go gl.startLogger()
|
||||
return gl
|
||||
}
|
||||
|
||||
// SetLogger provides a given logger adapter into Logger with config string.
|
||||
// config must in in JSON format like {"interval":360}}
|
||||
func (gl *Logger) setLogger(adapterName string, configs ...string) error {
|
||||
config := append(configs, "{}")[0]
|
||||
|
||||
logAdapter, ok := adapters[adapterName]
|
||||
if !ok {
|
||||
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName)
|
||||
}
|
||||
|
||||
lg := logAdapter()
|
||||
|
||||
err := lg.Init(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gl.outputs = append(gl.outputs, &nameLogger{name: adapterName, LogProvider: lg})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gl *Logger) writeToLoggers(lm *LogMsg) {
|
||||
for _, l := range gl.outputs {
|
||||
err := l.WriteMsg(lm)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (gl *Logger) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// writeMsg will always add a '\n' character
|
||||
if p[len(p)-1] == '\n' {
|
||||
p = p[0 : len(p)-1]
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Msg: string(p),
|
||||
When: time.Now(),
|
||||
}
|
||||
|
||||
// set levelLoggerImpl to ensure all log message will be write out
|
||||
err = gl.writeMsg(lm)
|
||||
if err == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (gl *Logger) writeMsg(lm *LogMsg) error {
|
||||
if gl.asynchronous {
|
||||
logM := logMsgPool.Get().(*LogMsg)
|
||||
logM.Msg = lm.Msg
|
||||
logM.When = lm.When
|
||||
|
||||
if gl.outputs != nil {
|
||||
gl.msgChan <- lm
|
||||
} else {
|
||||
logMsgPool.Put(lm)
|
||||
}
|
||||
} else {
|
||||
gl.writeToLoggers(lm)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gl *Logger) startLogger() {
|
||||
gameOver := false
|
||||
for {
|
||||
select {
|
||||
case bm, ok := <-gl.msgChan:
|
||||
if ok {
|
||||
gl.writeToLoggers(bm)
|
||||
logMsgPool.Put(bm)
|
||||
}
|
||||
case <-gl.closeChan:
|
||||
gl.flush()
|
||||
for _, l := range gl.outputs {
|
||||
l.Destroy()
|
||||
}
|
||||
gl.outputs = nil
|
||||
gameOver = true
|
||||
gl.wg.Done()
|
||||
case <-gl.flushChan:
|
||||
gl.flush()
|
||||
gl.wg.Done()
|
||||
}
|
||||
if gameOver {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info Log INFO level message.
|
||||
func (gl *Logger) Info(format string) {
|
||||
lm := &LogMsg{
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
}
|
||||
|
||||
gl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// BizErr Log a json-interface
|
||||
func (gl *Logger) BizErr(v interface{}) error {
|
||||
buf, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str := *(*string)(unsafe.Pointer(&buf))
|
||||
gl.Info(str)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush flush all chan data.
|
||||
func (gl *Logger) Flush() {
|
||||
if gl.asynchronous {
|
||||
gl.flushChan <- struct{}{}
|
||||
gl.wg.Wait()
|
||||
gl.wg.Add(1)
|
||||
return
|
||||
}
|
||||
gl.flush()
|
||||
}
|
||||
|
||||
// Close close logger, flush all chan data and destroy all adapters in Logger.
|
||||
func (gl *Logger) Close() {
|
||||
if gl.asynchronous {
|
||||
gl.closeChan <- struct{}{}
|
||||
gl.wg.Wait()
|
||||
close(gl.msgChan)
|
||||
} else {
|
||||
gl.flush()
|
||||
for _, l := range gl.outputs {
|
||||
l.Destroy()
|
||||
}
|
||||
gl.outputs = nil
|
||||
}
|
||||
close(gl.flushChan)
|
||||
close(gl.closeChan)
|
||||
}
|
||||
|
||||
func (gl *Logger) flush() {
|
||||
if gl.asynchronous {
|
||||
for {
|
||||
if len(gl.msgChan) > 0 {
|
||||
bm, ok := <-gl.msgChan
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
gl.writeToLoggers(bm)
|
||||
logMsgPool.Put(bm)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, l := range gl.outputs {
|
||||
l.Flush()
|
||||
}
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ServerLogOptions struct {
|
||||
// LogDir 日志目录,默认logs
|
||||
LogDir string
|
||||
|
||||
// EnableAsync 是否开启异步写日志,默认开启
|
||||
EnableAsync bool
|
||||
|
||||
// AsyncQueueSize 异步队列大小,默认1000
|
||||
AsyncQueueSize int64
|
||||
}
|
||||
|
||||
// NewServerLogger 业务日志实例,support JSON
|
||||
//
|
||||
// opts.LogDir 日志目录,推荐logs
|
||||
// opts.EnableAsync 是否开启异步写日志,默认开启
|
||||
// opts.AsyncQueueSize 异步队列大小,默认1000
|
||||
// useage: logger.BizErr(map[string]interface{}{"type": "test"})
|
||||
func NewServerLogger(opts *ServerLogOptions) (*Logger, error) {
|
||||
if opts == nil {
|
||||
opts = &ServerLogOptions{
|
||||
LogDir: "logs",
|
||||
EnableAsync: true,
|
||||
AsyncQueueSize: 1000,
|
||||
}
|
||||
}
|
||||
if opts.LogDir == "" {
|
||||
opts.LogDir = "logs"
|
||||
}
|
||||
if opts.EnableAsync && opts.AsyncQueueSize == 0 {
|
||||
opts.AsyncQueueSize = 1000
|
||||
}
|
||||
|
||||
l := newLogger()
|
||||
hostname, _ := os.Hostname()
|
||||
filename := fmt.Sprintf("server_%s.log", hostname)
|
||||
configs := fmt.Sprintf(`{"filename":"%s/%s"}`, strings.TrimRight(opts.LogDir, "/"), filename)
|
||||
err := l.setLogger(AdapterFile, configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.EnableAsync && opts.AsyncQueueSize > 0 {
|
||||
l.Async(opts.AsyncQueueSize)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
type ServerGALogOptions struct {
|
||||
// LogDir 日志目录,默认galogs
|
||||
LogDir string
|
||||
|
||||
// EnableAsync 是否开启异步写日志,默认开启
|
||||
EnableAsync bool
|
||||
|
||||
// AsyncQueueSize 异步队列大小,默认1000
|
||||
AsyncQueueSize int64
|
||||
|
||||
// GA侧分配ProjectID
|
||||
ProjectID string
|
||||
|
||||
// GA侧分配ClientID
|
||||
ClientID string
|
||||
|
||||
LogType LogType
|
||||
}
|
||||
|
||||
// NewServerGALogger GA日志实例,support JSON
|
||||
//
|
||||
// opts.ProjectID 项目ID,GA侧分配,必填
|
||||
// opts.ClientID 客户端ID,GA侧分配,必填
|
||||
// opts.LogType 日志类型,默认LogTypeTrack
|
||||
// opts.LogDir 日志目录,默认galogs
|
||||
// opts.EnableAsync 是否开启异步写日志,默认开启
|
||||
// opts.AsyncQueueSize 异步队列大小,默认1000
|
||||
func NewServerGALogger(opts *ServerGALogOptions) (*GALogger, error) {
|
||||
if opts == nil {
|
||||
return nil, errors.New("opts must not be empty")
|
||||
}
|
||||
if opts.EnableAsync && opts.AsyncQueueSize == 0 {
|
||||
opts.AsyncQueueSize = 1000
|
||||
}
|
||||
if opts.LogDir == "" {
|
||||
opts.LogDir = "galogs"
|
||||
}
|
||||
if opts.LogType == "" {
|
||||
opts.LogType = LogTypeTrack
|
||||
}
|
||||
if opts.ProjectID == "" || opts.ClientID == "" {
|
||||
return nil, errors.New("projectID or clientID must not be empty")
|
||||
}
|
||||
|
||||
l := newLogger()
|
||||
hostname, _ := os.Hostname()
|
||||
filename := fmt.Sprintf("ga_%s.log", hostname)
|
||||
if opts.LogType == LogTypeAsset {
|
||||
filename = fmt.Sprintf("asset_%s.log", hostname)
|
||||
}
|
||||
configs := fmt.Sprintf(`{"filename":"%s/%s"}`, strings.TrimRight(opts.LogDir, "/"), filename)
|
||||
err := l.setLogger(AdapterFile, configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.EnableAsync && opts.AsyncQueueSize > 0 {
|
||||
l.Async(opts.AsyncQueueSize)
|
||||
}
|
||||
gaLogger := &GALogger{
|
||||
logger: l,
|
||||
projectID: opts.ProjectID,
|
||||
clientID: opts.ClientID,
|
||||
logType: opts.LogType,
|
||||
}
|
||||
|
||||
return gaLogger, nil
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package galog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var slogger *Logger
|
||||
|
||||
// go test -bench . -benchmem
|
||||
func init() {
|
||||
slogger, _ = NewServerLogger(&ServerLogOptions{
|
||||
LogDir: "logs",
|
||||
EnableAsync: true,
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkServerLog-8 436682 3103 ns/op 1838 B/op 24 allocs/op
|
||||
func BenchmarkServerLog(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
msgMap := map[string]interface{}{
|
||||
"CreateTime": time.Now().UnixNano() / int64(time.Microsecond),
|
||||
"Host": "host",
|
||||
"AppId": "10010",
|
||||
"UserId": "12345",
|
||||
"Level": "Notice",
|
||||
"Entry": "Login",
|
||||
"Func": "HandleLogin",
|
||||
"TraceMsg": "HandlerWSFriApplyList|HandlerWSFriApplyList",
|
||||
"Params": "{\"isoCode\":\"CN\",\"P0\":[3,\"Total: 3, End: 0, \"],\"pf\":\"wx\",\"appVer\":\"1.0\",\"sdkVer\":\"1.0\",\"ip\":\"58.247.195.158\",\"clientId\":\"7abd64bf-3fb6-4fef-a25e-e6562b7fb857\",\"timeZone\":\"Asia/Shanghai\",\"loginMark\":\"\",\"st\":1472}",
|
||||
}
|
||||
slogger.BizErr(msgMap)
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"server/MergeConst"
|
||||
"server/conf"
|
||||
userCfg "server/conf/user"
|
||||
@ -25,9 +26,10 @@ import (
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"server/pkg/github.com/name5566/leaf/gate"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"server/pkg/github.com/name5566/leaf/timer"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/gate"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/timer"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -36,6 +38,7 @@ var (
|
||||
|
||||
var isInitGameLogic = false
|
||||
var RegisterNetWorkFunc = make(map[string]interface{})
|
||||
var NewRegisterNetWorkFunc = make(map[string]func(*Player, *proto.Message) error)
|
||||
|
||||
const (
|
||||
SERVER_STATUS_OPEN = 1 // 服务器状态 开放
|
||||
@ -48,6 +51,82 @@ func RegisterMsgProcessFunc(key string, value1 interface{}) {
|
||||
RegisterNetWorkFunc[key] = value1
|
||||
}
|
||||
|
||||
func RegisterNewMsgProcessFunc(key string, value interface{}) {
|
||||
handler, err := buildNewMsgHandlerAdapter(value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("RegisterNewMsgProcessFunc[%s] invalid handler: %v", key, err))
|
||||
}
|
||||
NewRegisterNetWorkFunc[key] = handler
|
||||
}
|
||||
|
||||
func buildNewMsgHandlerAdapter(value interface{}) (func(*Player, *proto.Message) error, error) {
|
||||
if fn, ok := value.(func(*Player, *proto.Message) error); ok {
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("handler is nil")
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(value)
|
||||
rt := rv.Type()
|
||||
if rt.Kind() != reflect.Func {
|
||||
return nil, fmt.Errorf("handler must be function, got %s", rt.Kind())
|
||||
}
|
||||
if rt.NumIn() != 2 || rt.NumOut() != 1 {
|
||||
return nil, fmt.Errorf("handler signature must be func(*Player, *T) error")
|
||||
}
|
||||
if rt.In(0) != reflect.TypeOf(&Player{}) {
|
||||
return nil, fmt.Errorf("first arg must be *Player")
|
||||
}
|
||||
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
||||
if !rt.Out(0).Implements(errorType) {
|
||||
return nil, fmt.Errorf("return type must be error")
|
||||
}
|
||||
|
||||
msgIfaceType := reflect.TypeOf((*proto.Message)(nil)).Elem()
|
||||
msgArgType := rt.In(1)
|
||||
if msgArgType == reflect.TypeOf((*proto.Message)(nil)) {
|
||||
return func(player *Player, msg *proto.Message) error {
|
||||
results := rv.Call([]reflect.Value{reflect.ValueOf(player), reflect.ValueOf(msg)})
|
||||
if results[0].IsNil() {
|
||||
return nil
|
||||
}
|
||||
return results[0].Interface().(error)
|
||||
}, nil
|
||||
}
|
||||
if msgArgType.Kind() != reflect.Ptr || !msgArgType.Implements(msgIfaceType) {
|
||||
return nil, fmt.Errorf("second arg must be *proto.Message or pointer type implementing proto.Message")
|
||||
}
|
||||
|
||||
return func(player *Player, msg *proto.Message) error {
|
||||
if msg == nil || *msg == nil {
|
||||
return fmt.Errorf("nil proto message")
|
||||
}
|
||||
|
||||
raw := *msg
|
||||
rawType := reflect.TypeOf(raw)
|
||||
if !rawType.AssignableTo(msgArgType) {
|
||||
return fmt.Errorf("message type mismatch, expect %s got %s", msgArgType, rawType)
|
||||
}
|
||||
|
||||
results := rv.Call([]reflect.Value{reflect.ValueOf(player), reflect.ValueOf(raw)})
|
||||
if results[0].IsNil() {
|
||||
return nil
|
||||
}
|
||||
return results[0].Interface().(error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RunNewNetProcessByKey(key string, player *Player, msg *proto.Message) error {
|
||||
fun, ok := NewRegisterNetWorkFunc[key]
|
||||
if ok {
|
||||
err := fun(player, msg)
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("cant find network func %s", key)
|
||||
}
|
||||
|
||||
func RunNetProcessByKey(key string, param []interface{}) error {
|
||||
fun, ok := RegisterNetWorkFunc[key]
|
||||
if ok {
|
||||
@ -208,7 +287,8 @@ func (ad *GameLogic) NewAccountInsertDataToDB() bool {
|
||||
ModData: buf.Bytes(),
|
||||
UpdataTime: int32(time.Now().Unix()),
|
||||
}
|
||||
db.FormatAllMemInsertDb(playerMod, "t_player_mod")
|
||||
tableName := "t_player_mod_" + fmt.Sprintf("%02d", insertId%100)
|
||||
db.FormatAllMemInsertDb(playerMod, tableName)
|
||||
// 创建玩家日志
|
||||
player := new(Player)
|
||||
BaseMod := player.PlayMod.getBaseMod()
|
||||
@ -565,6 +645,8 @@ func (ad *GameLogic) ReplaceExistPlayerAndAgent(a gate.Agent, player *Player) er
|
||||
if agent != nil && a != agent {
|
||||
G_getGameLogic().PackResInfo(agent, "ForceKickOut", data)
|
||||
internal.AsignPlayerToAgents(agent, ad.NotInitPlayer)
|
||||
agent.Close()
|
||||
log.Debug("player %d 被挤下线", player.M_DwUin)
|
||||
}
|
||||
internal.AsignPlayerToAgents(a, player)
|
||||
player.SetAgent(a)
|
||||
@ -636,239 +718,244 @@ func (ad *GameLogic) RemoveOldLogs(days int) {
|
||||
|
||||
func (ad *GameLogic) RegisterNetWorkFunc() {
|
||||
|
||||
RegisterMsgProcessFunc("ReqRemoveAd", ReqRemoveAdFunc)
|
||||
RegisterMsgProcessFunc("ReqPlayerBriefProfileData", ReqPlayerBriefProfileDataFunc)
|
||||
RegisterMsgProcessFunc("ReqFriendPlayerSimple", ReqFriendPlayerSimple)
|
||||
RegisterNewMsgProcessFunc("ReqRemoveAd", ReqRemoveAd)
|
||||
RegisterNewMsgProcessFunc("ReqPlayerBriefProfileData", ReqPlayerBriefProfileData)
|
||||
RegisterNewMsgProcessFunc("ReqFriendPlayerSimple", ReqFriendPlayerSimple)
|
||||
// RegisterMsgProcessFunc("ReqOfflineReconnect", ReqOfflineReconnectFunc)
|
||||
RegisterMsgProcessFunc("ReqPlayerAsset", ReqPlayerAsset)
|
||||
RegisterMsgProcessFunc("ReqId2Verify", ReqId2Verify) // 身份证验证
|
||||
RegisterNewMsgProcessFunc("ReqPlayerAsset", ReqPlayerAsset)
|
||||
RegisterNewMsgProcessFunc("ReqId2Verify", ReqId2Verify) // 身份证验证
|
||||
// 玩家
|
||||
RegisterMsgProcessFunc("ReqUserInfo", ReqUserInfo)
|
||||
RegisterMsgProcessFunc("ReqSetName", ReqSetName) // 设置名字
|
||||
RegisterMsgProcessFunc("ReqLang", ReqLang) // 设置语言
|
||||
RegisterMsgProcessFunc("ReqSetPetName", ReqSetPetName) // 设置宠物名字
|
||||
RegisterMsgProcessFunc("ReqSetFacebookUrl", ReqSetFacebookUrl) // 设置facebook地址
|
||||
RegisterMsgProcessFunc("ReqPlayerBaseInfo", ReqPlayerBaseInfofunction) // 请求玩家基本信息
|
||||
RegisterMsgProcessFunc("ReqKv", ReqKv) // 保存客户端数据
|
||||
RegisterMsgProcessFunc("ReqGetEnergyByAD", ReqGetEnergyByAD) // 看广告获取能量
|
||||
RegisterMsgProcessFunc("ReqBuyEnergy", ReqBuyEnergy) // 购买能量
|
||||
RegisterMsgProcessFunc("ReqAdWatch", ReqAdWatch) // 观看广告
|
||||
RegisterNewMsgProcessFunc("ReqUserInfo", ReqUserInfo)
|
||||
RegisterNewMsgProcessFunc("ReqSetName", ReqSetName) // 设置名字
|
||||
RegisterNewMsgProcessFunc("ReqLang", ReqLang) // 设置语言
|
||||
RegisterNewMsgProcessFunc("ReqSetPetName", ReqSetPetName) // 设置宠物名字
|
||||
RegisterNewMsgProcessFunc("ReqSetFacebookUrl", ReqSetFacebookUrl) // 设置facebook地址
|
||||
RegisterNewMsgProcessFunc("ReqPlayerBaseInfo", ReqPlayerBaseInfo) // 请求玩家基本信息
|
||||
RegisterNewMsgProcessFunc("ReqKv", ReqKv) // 保存客户端数据
|
||||
RegisterNewMsgProcessFunc("ReqGetEnergyByAD", ReqGetEnergyByAD) // 看广告获取能量
|
||||
RegisterNewMsgProcessFunc("ReqBuyEnergy", ReqBuyEnergy) // 购买能量
|
||||
RegisterNewMsgProcessFunc("ReqAdWatch", ReqAdWatch) // 观看广告
|
||||
// #region 棋盘
|
||||
RegisterMsgProcessFunc("ReqPlayerChessData", ReqPlayerChessDataFunc)
|
||||
RegisterMsgProcessFunc("UpdatePlayerChessData", UpdatePlayerChessDataFunc) // 更新棋盘数据
|
||||
RegisterMsgProcessFunc("ReqSetEnergyMul", RegSetEneryFunc) //设置能量倍数
|
||||
RegisterMsgProcessFunc("ReqChessEx", ReqChessEx) // 转换棋子
|
||||
RegisterMsgProcessFunc("ReqGetChessFromBuff", ReqGetChessFromBuff) // 从buff中获取棋子
|
||||
RegisterMsgProcessFunc("ReqPutPartInBag", ReqPutPartInBag) // 把零件放入背包
|
||||
RegisterMsgProcessFunc("ReqPutChessInBag", ReqPutChessInBag) // 把棋子放入背包
|
||||
RegisterMsgProcessFunc("ReqTakeChessOutBag", ReqTakeChessOutBag) // 从背包中取出棋子
|
||||
RegisterMsgProcessFunc("ReqTakeChessOutBagToHonor", ReqTakeChessOutBagToHonor) // 从背包中取出棋子
|
||||
RegisterMsgProcessFunc("ReqBuyChessBagGrid", ReqBuyChessBagGrid) // 解锁背包格子
|
||||
RegisterMsgProcessFunc("ReqSourceChest", ReqSourceChest) // 开宝箱
|
||||
RegisterMsgProcessFunc("ReqSeparateChess", ReqSeparateChess) // 分解棋子
|
||||
RegisterMsgProcessFunc("ReqUpgradeChess", ReqUpgradeChess) // 升级棋子
|
||||
RegisterMsgProcessFunc("ReqSellChessNum", ReqSellChessNum) //购买能量
|
||||
RegisterMsgProcessFunc("ReqGetChessRetireReward", ReqGetChessRetireReward) //领取棋子退役奖励
|
||||
RegisterNewMsgProcessFunc("ReqPlayerChessData", ReqPlayerChessData)
|
||||
RegisterNewMsgProcessFunc("UpdatePlayerChessData", UpdatePlayerChessData) // 更新棋盘数据
|
||||
RegisterNewMsgProcessFunc("ReqSetEnergyMul", ReqSetEnergyMul) //设置能量倍数
|
||||
RegisterNewMsgProcessFunc("ReqChessEx", ReqChessEx) // 转换棋子
|
||||
RegisterNewMsgProcessFunc("ReqGetChessFromBuff", ReqGetChessFromBuff) // 从buff中获取棋子
|
||||
RegisterNewMsgProcessFunc("ReqPutPartInBag", ReqPutPartInBag) // 把零件放入背包
|
||||
RegisterNewMsgProcessFunc("ReqPutChessInBag", ReqPutChessInBag) // 把棋子放入背包
|
||||
RegisterNewMsgProcessFunc("ReqTakeChessOutBag", ReqTakeChessOutBag) // 从背包中取出棋子
|
||||
RegisterNewMsgProcessFunc("ReqTakeChessOutBagToHonor", ReqTakeChessOutBagToHonor) // 从背包中取出棋子
|
||||
RegisterNewMsgProcessFunc("ReqBuyChessBagGrid", ReqBuyChessBagGrid) // 解锁背包格子
|
||||
RegisterNewMsgProcessFunc("ReqSourceChest", ReqSourceChest) // 开宝箱
|
||||
RegisterNewMsgProcessFunc("ReqSeparateChess", ReqSeparateChess) // 分解棋子
|
||||
RegisterNewMsgProcessFunc("ReqUpgradeChess", ReqUpgradeChess) // 升级棋子
|
||||
RegisterNewMsgProcessFunc("ReqSellChessNum", ReqSellChessNum) //购买能量
|
||||
RegisterNewMsgProcessFunc("ReqGetChessRetireReward", ReqGetChessRetireReward) //领取棋子退役奖励
|
||||
|
||||
//领取图鉴奖励
|
||||
RegisterMsgProcessFunc("ReqGetHandbookReward", ReqGetHandbookReward) //领取图鉴奖励
|
||||
RegisterMsgProcessFunc("RegHandbookAllReward", RegHandbookAllReward) //领取图鉴收集奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetHandbookReward", ReqGetHandbookReward) //领取图鉴奖励
|
||||
RegisterNewMsgProcessFunc("RegHandbookAllReward", RegHandbookAllReward) //领取图鉴收集奖励
|
||||
//领取订单奖励
|
||||
RegisterMsgProcessFunc("ReqRewardOrder", ReqRewardOrder) // 领取订单奖励
|
||||
RegisterMsgProcessFunc("ReqDelOrder", ReqDelOrder) // 删除订单
|
||||
RegisterMsgProcessFunc("ReqCreatePetOrder", ReqCreatePetOrder) // 生成消耗品订单
|
||||
RegisterNewMsgProcessFunc("ReqRewardOrder", ReqRewardOrder) // 领取订单奖励
|
||||
RegisterNewMsgProcessFunc("ReqDelOrder", ReqDelOrder) // 删除订单
|
||||
RegisterNewMsgProcessFunc("ReqCreatePetOrder", ReqCreatePetOrder) // 生成消耗品订单
|
||||
|
||||
//装饰
|
||||
RegisterMsgProcessFunc("ReqDecorate", ReqDecorate) // 装饰
|
||||
RegisterMsgProcessFunc("ReqDecorateAll", ReqDecorateAll) // 装饰全部
|
||||
RegisterMsgProcessFunc("ReqAreaReward", ReqAreaReward) // 章节奖励
|
||||
RegisterNewMsgProcessFunc("ReqDecorate", ReqDecorate) // 装饰
|
||||
RegisterNewMsgProcessFunc("ReqDecorateAll", ReqDecorateAll) // 装饰全部
|
||||
RegisterNewMsgProcessFunc("ReqAreaReward", ReqAreaReward) // 章节奖励
|
||||
//Gm命令
|
||||
RegisterMsgProcessFunc("ReqGmCommand", ReqGmCommand) // Gm命令
|
||||
RegisterNewMsgProcessFunc("ReqGmCommand", ReqGmCommand) // Gm命令
|
||||
|
||||
// #region 卡牌
|
||||
RegisterMsgProcessFunc("ReqCardInfo", ReqCardInfo) // 请求卡牌信息
|
||||
RegisterMsgProcessFunc("ReqCardSeasonFirstReward", ReqCardSeasonFirstReward) // 领取赛季首次奖励
|
||||
RegisterMsgProcessFunc("ReqCardCollectReward", ReqCardCollectReward) //领取卡牌系列收集奖励
|
||||
RegisterMsgProcessFunc("ReqExStarReward", ReqExStarReward) // 兑换收集星星奖励
|
||||
RegisterMsgProcessFunc("ReqAllCollectReward", ReqAllCollectReward) // 领取全收集奖励
|
||||
RegisterMsgProcessFunc("ReqCardGive", ReqCardGive) // 请求赠送卡牌
|
||||
RegisterMsgProcessFunc("ReqAgreeCardGive", ReqAgreeCardGive) // 同意赠送卡牌
|
||||
RegisterMsgProcessFunc("ReqRefuseCardGive", ReqRefuseCardGive) // 拒绝赠送卡牌
|
||||
RegisterNewMsgProcessFunc("ReqCardInfo", ReqCardInfo) // 请求卡牌信息
|
||||
RegisterNewMsgProcessFunc("ReqCardSeasonFirstReward", ReqCardSeasonFirstReward) // 领取赛季首次奖励
|
||||
RegisterNewMsgProcessFunc("ReqCardCollectReward", ReqCardCollectReward) //领取卡牌系列收集奖励
|
||||
RegisterNewMsgProcessFunc("ReqExStarReward", ReqExStarReward) // 兑换收集星星奖励
|
||||
RegisterNewMsgProcessFunc("ReqAllCollectReward", ReqAllCollectReward) // 领取全收集奖励
|
||||
RegisterNewMsgProcessFunc("ReqCardGive", ReqCardGive) // 请求赠送卡牌
|
||||
RegisterNewMsgProcessFunc("ReqAgreeCardGive", ReqAgreeCardGive) // 同意赠送卡牌
|
||||
RegisterNewMsgProcessFunc("ReqRefuseCardGive", ReqRefuseCardGive) // 拒绝赠送卡牌
|
||||
|
||||
RegisterMsgProcessFunc("ReqCardExchange", ReqCardExchange) // 请求交换卡牌
|
||||
RegisterMsgProcessFunc("ReqSelectCardExchange", ReqSelectCardExchange) // 选择交换的卡牌
|
||||
RegisterMsgProcessFunc("ReqAgreeCardExchange", ReqAgreeCardExchange) // 完成交换卡牌
|
||||
RegisterMsgProcessFunc("ReqRefuseCardSelect", ReqRefuseCardSelect) // 拒绝选择卡牌进行交换
|
||||
RegisterMsgProcessFunc("ReqRefuseCardExchange", ReqRefuseCardExchange) // 拒绝卡牌交换
|
||||
RegisterMsgProcessFunc("ReqCardSend", ReqCardSend) // 直接赠送卡牌
|
||||
RegisterMsgProcessFunc("ReqGetFriendCard", ReqGetFriendCard) // 领取好友赠送的卡牌
|
||||
RegisterMsgProcessFunc("ReqMasterCard", ReqMasterCard) // 万能卡兑换
|
||||
RegisterMsgProcessFunc("ReqCardHandbookReward", ReqCardHandbookReward) // 卡牌图鉴
|
||||
RegisterNewMsgProcessFunc("ReqCardExchange", ReqCardExchange) // 请求交换卡牌
|
||||
RegisterNewMsgProcessFunc("ReqSelectCardExchange", ReqSelectCardExchange) // 选择交换的卡牌
|
||||
RegisterNewMsgProcessFunc("ReqAgreeCardExchange", ReqAgreeCardExchange) // 完成交换卡牌
|
||||
RegisterNewMsgProcessFunc("ReqRefuseCardSelect", ReqRefuseCardSelect) // 拒绝选择卡牌进行交换
|
||||
RegisterNewMsgProcessFunc("ReqRefuseCardExchange", ReqRefuseCardExchange) // 拒绝卡牌交换
|
||||
RegisterNewMsgProcessFunc("ReqCardSend", ReqCardSend) // 直接赠送卡牌
|
||||
RegisterNewMsgProcessFunc("ReqGetFriendCard", ReqGetFriendCard) // 领取好友赠送的卡牌
|
||||
RegisterNewMsgProcessFunc("ReqMasterCard", ReqMasterCard) // 万能卡兑换
|
||||
RegisterNewMsgProcessFunc("ReqCardHandbookReward", ReqCardHandbookReward) // 卡牌图鉴
|
||||
|
||||
// 日常任务
|
||||
RegisterMsgProcessFunc("ReqGetDailyTaskReward", ReqGetDailyTaskReward) // 领取日常任务奖励
|
||||
RegisterMsgProcessFunc("ReqGetDailyWeekReward", ReqGetDailyWeekReward) // 领取周活跃奖励
|
||||
RegisterMsgProcessFunc("ReqDailyUnlock", ReqDailyUnlock) // 日常任务解锁
|
||||
RegisterNewMsgProcessFunc("ReqGetDailyTaskReward", ReqGetDailyTaskReward) // 领取日常任务奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetDailyWeekReward", ReqGetDailyWeekReward) // 领取周活跃奖励
|
||||
RegisterNewMsgProcessFunc("ReqDailyUnlock", ReqDailyUnlock) // 日常任务解锁
|
||||
// 新手任务
|
||||
RegisterMsgProcessFunc("ReqGetGuideTaskReward", ReqGetGuideTaskReward) // 领取日新手任务奖励
|
||||
RegisterMsgProcessFunc("ReqGetGuideActiveReward", ReqGetGuideActiveReward) // 领取活跃奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetGuideTaskReward", ReqGetGuideTaskReward) // 领取日新手任务奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetGuideActiveReward", ReqGetGuideActiveReward) // 领取活跃奖励
|
||||
|
||||
// 引导奖励
|
||||
RegisterMsgProcessFunc("ReqGuideReward", ReqGuideReward) // 领取引导奖励
|
||||
RegisterMsgProcessFunc("ReqGuidePlayroom", ReqGuidePlayroom) // 领取playroom引导奖励
|
||||
RegisterNewMsgProcessFunc("ReqGuideReward", ReqGuideReward) // 领取引导奖励
|
||||
RegisterNewMsgProcessFunc("ReqGuidePlayroom", ReqGuidePlayroom) // 领取playroom引导奖励
|
||||
|
||||
// 头像
|
||||
RegisterMsgProcessFunc("ReqSetFace", ReqSetFace) // 设置头像
|
||||
RegisterNewMsgProcessFunc("ReqSetFace", ReqSetFace) // 设置头像
|
||||
// 头像框
|
||||
RegisterMsgProcessFunc("ReqSetAvatar", ReqSetAvatar) // 设置头像框
|
||||
RegisterNewMsgProcessFunc("ReqSetAvatar", ReqSetAvatar) // 设置头像框
|
||||
// 表情
|
||||
RegisterMsgProcessFunc("ReqSetEmoji", ReqSetEmoji) // 设置表情
|
||||
RegisterNewMsgProcessFunc("ReqSetEmoji", ReqSetEmoji) // 设置表情
|
||||
// 收藏室
|
||||
RegisterMsgProcessFunc("ReqCollectInfo", ReqCollectInfo) // 请求收藏室数据
|
||||
RegisterMsgProcessFunc("ReqCollect", ReqCollect) // 领取收藏室奖励
|
||||
RegisterNewMsgProcessFunc("ReqCollectInfo", ReqCollectInfo) // 请求收藏室数据
|
||||
RegisterNewMsgProcessFunc("ReqCollect", ReqCollect) // 领取收藏室奖励
|
||||
// 七日签到
|
||||
RegisterMsgProcessFunc("ReqGetSevenLoginReward", ReqGetSevenLoginReward) // 领取七日签到奖励
|
||||
RegisterMsgProcessFunc("ReqGetMonthLoginReward", ReqGetMonthLoginReward) // 领取月签到奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetSevenLoginReward", ReqGetSevenLoginReward) // 领取七日签到奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetMonthLoginReward", ReqGetMonthLoginReward) // 领取月签到奖励
|
||||
|
||||
// 限时事件
|
||||
RegisterMsgProcessFunc("ReqLimitEvent", ReqLimitEvent) // 请求限时事件数据
|
||||
RegisterMsgProcessFunc("ReqFastProduceReward", ReqFastProduceReward) // 连击快手奖励
|
||||
RegisterMsgProcessFunc("ReqFastProduceInfo", ReqFastProduceInfo) // 请求连击快手数据
|
||||
RegisterMsgProcessFunc("ReqLimitSenceReward", ReqLimitSenceReward) // 获取场景转盘奖励
|
||||
RegisterMsgProcessFunc("ReqSelectLimitEvent", ReqSelectLimitEvent) //领取Bouns限时事件进度奖励
|
||||
RegisterMsgProcessFunc("ReqGetGoldCard", ReqGetGoldCard) //请求金卡交换信息
|
||||
RegisterMsgProcessFunc("ReqLimitEventLuckyCat", ReqLimitEventLuckyCat) //幸运猫获取奖励
|
||||
RegisterMsgProcessFunc("ReqCatTrickReward", ReqCatTrickReward) //小猫戏法获取奖励
|
||||
RegisterNewMsgProcessFunc("ReqLimitEvent", ReqLimitEvent) // 请求限时事件数据
|
||||
RegisterNewMsgProcessFunc("ReqFastProduceReward", ReqFastProduceReward) // 连击快手奖励
|
||||
RegisterNewMsgProcessFunc("ReqFastProduceInfo", ReqFastProduceInfo) // 请求连击快手数据
|
||||
RegisterNewMsgProcessFunc("ReqLimitSenceReward", ReqLimitSenceReward) // 获取场景转盘奖励
|
||||
RegisterNewMsgProcessFunc("ReqSelectLimitEvent", ReqSelectLimitEvent) //领取Bouns限时事件进度奖励
|
||||
RegisterNewMsgProcessFunc("ReqGetGoldCard", ReqGetGoldCard) //请求金卡交换信息
|
||||
RegisterNewMsgProcessFunc("ReqLimitEventLuckyCat", ReqLimitEventLuckyCat) //幸运猫获取奖励
|
||||
RegisterNewMsgProcessFunc("ReqCatTrickReward", ReqCatTrickReward) //小猫戏法获取奖励
|
||||
|
||||
// #region 好友
|
||||
RegisterMsgProcessFunc("ReqFriendList", ReqFriendList) // 请求好友列表
|
||||
RegisterMsgProcessFunc("ReqFriendApply", ReqFriendApply) // 请求申请好友列表
|
||||
RegisterMsgProcessFunc("ReqFriendCardMsg", ReqFriendCardMsg) // 请求好友卡牌申请列表
|
||||
RegisterMsgProcessFunc("ReqWishApplyList", ReqWishApplyList) // 请求好友心愿单申请列表
|
||||
RegisterMsgProcessFunc("ReqFriendTimeLine", ReqFriendTimeLine) // 请求好友时间线
|
||||
RegisterMsgProcessFunc("ReqFriendRecommend", ReqFriendRecommend) // 获取推荐好友
|
||||
RegisterMsgProcessFunc("ReqFriendTLUpvote", ReqFriendTLUpvote) // 请求时间线点赞
|
||||
RegisterMsgProcessFunc("ReqFriendTReward", ReqFriendTReward) // 获取时间线奖励
|
||||
RegisterMsgProcessFunc("ReqAddNpc", ReqAddNpc) // 增加npc
|
||||
RegisterMsgProcessFunc("ReqWishApply", ReqWishApply) // 同意好友心愿单请求
|
||||
RegisterMsgProcessFunc("ReqFriendByCode", ReqFriendByCode) // 根据邀请码查询好友
|
||||
RegisterMsgProcessFunc("ReqFriendReplyHandle", ReqFriendReplyHandle) // 回复好友信息
|
||||
RegisterNewMsgProcessFunc("ReqFriendList", ReqFriendList) // 请求好友列表
|
||||
RegisterNewMsgProcessFunc("ReqFriendApply", ReqFriendApply) // 请求申请好友列表
|
||||
RegisterNewMsgProcessFunc("ReqFriendCardMsg", ReqFriendCardMsg) // 请求好友卡牌申请列表
|
||||
RegisterNewMsgProcessFunc("ReqWishApplyList", ReqWishApplyList) // 请求好友心愿单申请列表
|
||||
RegisterNewMsgProcessFunc("ReqFriendTimeLine", ReqFriendTimeLine) // 请求好友时间线
|
||||
RegisterNewMsgProcessFunc("ReqFriendRecommend", ReqFriendRecommend) // 获取推荐好友
|
||||
RegisterNewMsgProcessFunc("ReqFriendTLUpvote", ReqFriendTLUpvote) // 请求时间线点赞
|
||||
RegisterNewMsgProcessFunc("ReqFriendTReward", ReqFriendTReward) // 获取时间线奖励
|
||||
RegisterNewMsgProcessFunc("ReqAddNpc", ReqAddNpc) // 增加npc
|
||||
RegisterNewMsgProcessFunc("ReqWishApply", ReqWishApply) // 同意好友心愿单请求
|
||||
RegisterNewMsgProcessFunc("ReqFriendByCode", ReqFriendByCode) // 根据邀请码查询好友
|
||||
RegisterNewMsgProcessFunc("ReqFriendReplyHandle", ReqFriendReplyHandle) // 回复好友信息
|
||||
|
||||
RegisterMsgProcessFunc("ReqSearchPlayer", ReqSearchPlayer) // 搜索好友
|
||||
RegisterMsgProcessFunc("ReqApplyFriend", ReqApplyFriend) // 申请好友
|
||||
RegisterMsgProcessFunc("ReqAgreeFriend", ReqAgreeFriend) // 同意申请
|
||||
RegisterMsgProcessFunc("ReqRefuseFriend", ReqRefuseFriend) // 拒绝申请
|
||||
RegisterMsgProcessFunc("ReqDelFriend", ReqDelFriend) // 删除好友
|
||||
RegisterNewMsgProcessFunc("ReqSearchPlayer", ReqSearchPlayer) // 搜索好友
|
||||
RegisterNewMsgProcessFunc("ReqApplyFriend", ReqApplyFriend) // 申请好友
|
||||
RegisterNewMsgProcessFunc("ReqAgreeFriend", ReqAgreeFriend) // 同意申请
|
||||
RegisterNewMsgProcessFunc("ReqRefuseFriend", ReqRefuseFriend) // 拒绝申请
|
||||
RegisterNewMsgProcessFunc("ReqDelFriend", ReqDelFriend) // 删除好友
|
||||
|
||||
// Facebook邀请好友
|
||||
RegisterMsgProcessFunc("ReqInviteFriendData", ReqInviteFriendData) // 请求邀请好友数据
|
||||
RegisterMsgProcessFunc("ReqSelfInvited", ReqSelfInvited) // 请求自己邀请的好友
|
||||
RegisterMsgProcessFunc("ReqGetInviteReward", ReqGetInviteReward) // 领取邀请奖励
|
||||
RegisterMsgProcessFunc("ReqAutoAddInviteFriend", ReqAutoAddInviteFriend) // 自动添加邀请好友
|
||||
RegisterMsgProcessFunc("ReqAutoAddInviteFriend2", ReqAutoAddInviteFriend2) // 自动添加邀请好友
|
||||
RegisterMsgProcessFunc("ReqBindFacebookAccount", ReqBindFacebookAccount) // 绑定facebook账号
|
||||
RegisterMsgProcessFunc("ReqOnlyBindFacebook", ReqOnlyBindFacebook) // 绑定唯一facebook
|
||||
RegisterMsgProcessFunc("ReqUnBindFacebook", ReqUnBindFacebook) // 解绑facebook
|
||||
RegisterMsgProcessFunc("ReqSynGameData", ReqSynGameData) // 同步账号数据
|
||||
RegisterNewMsgProcessFunc("ReqInviteFriendData", ReqInviteFriendData) // 请求邀请好友数据
|
||||
RegisterNewMsgProcessFunc("ReqSelfInvited", ReqSelfInvited) // 请求自己邀请的好友
|
||||
RegisterNewMsgProcessFunc("ReqGetInviteReward", ReqGetInviteReward) // 领取邀请奖励
|
||||
RegisterNewMsgProcessFunc("ReqAutoAddInviteFriend", ReqAutoAddInviteFriend) // 自动添加邀请好友
|
||||
RegisterNewMsgProcessFunc("ReqAutoAddInviteFriend2", ReqAutoAddInviteFriend2) // 自动添加邀请好友
|
||||
RegisterNewMsgProcessFunc("ReqBindFacebookAccount", ReqBindFacebookAccount) // 绑定facebook账号
|
||||
RegisterNewMsgProcessFunc("ReqOnlyBindFacebook", ReqOnlyBindFacebook) // 绑定唯一facebook
|
||||
RegisterNewMsgProcessFunc("ReqUnBindFacebook", ReqUnBindFacebook) // 解绑facebook
|
||||
RegisterNewMsgProcessFunc("ReqSynGameData", ReqSynGameData) // 同步账号数据
|
||||
|
||||
// 榜单
|
||||
RegisterMsgProcessFunc("ReqRank", ReqRank) // 请求榜单数据
|
||||
RegisterNewMsgProcessFunc("ReqRank", ReqRank) // 请求榜单数据
|
||||
|
||||
// 邮件
|
||||
RegisterMsgProcessFunc("ReqMailList", ReqMailList) // 请求邮件数据
|
||||
RegisterMsgProcessFunc("ReqReadMail", ReqReadMail) // 读取邮件
|
||||
RegisterMsgProcessFunc("ReqGetMailReward", ReqGetMailReward) // 领取邮件奖励
|
||||
RegisterMsgProcessFunc("ReqDeleteMail", ReqDeleteMail) // 删除邮件
|
||||
RegisterNewMsgProcessFunc("ReqMailList", ReqMailList) // 请求邮件数据
|
||||
RegisterNewMsgProcessFunc("ReqReadMail", ReqReadMail) // 读取邮件
|
||||
RegisterNewMsgProcessFunc("ReqGetMailReward", ReqGetMailReward) // 领取邮件奖励
|
||||
RegisterNewMsgProcessFunc("ReqDeleteMail", ReqDeleteMail) // 删除邮件
|
||||
|
||||
// 商店
|
||||
RegisterMsgProcessFunc("ReqFreeShop", ReqFreeShop) // 领取商店免费奖励
|
||||
RegisterMsgProcessFunc("ReqBuyChessShop", ReqBuyChessShop) // 购买商店棋子
|
||||
RegisterMsgProcessFunc("ReqBuyChessShop2", ReqBuyChessShop2) // 购买商店棋子直接加入棋盘
|
||||
RegisterMsgProcessFunc("ReqRefreshChessShop", ReqRefreshChessShop) // 刷新棋子商店
|
||||
RegisterMsgProcessFunc("ReqAddWish", ReqAddWish) // 添加心愿单
|
||||
RegisterMsgProcessFunc("ReqGetWish", ReqGetWish) // 领取心愿单奖励
|
||||
RegisterMsgProcessFunc("ReqSendWishBeg", ReqSendWishBeg) // 发送心愿单请求
|
||||
RegisterNewMsgProcessFunc("ReqFreeShop", ReqFreeShop) // 领取商店免费奖励
|
||||
RegisterNewMsgProcessFunc("ReqBuyChessShop", ReqBuyChessShop) // 购买商店棋子
|
||||
RegisterNewMsgProcessFunc("ReqBuyChessShop2", ReqBuyChessShop2) // 购买商店棋子直接加入棋盘
|
||||
RegisterNewMsgProcessFunc("ReqRefreshChessShop", ReqRefreshChessShop) // 刷新棋子商店
|
||||
RegisterNewMsgProcessFunc("ReqAddWish", ReqAddWish) // 添加心愿单
|
||||
RegisterNewMsgProcessFunc("ReqGetWish", ReqGetWish) // 领取心愿单奖励
|
||||
RegisterNewMsgProcessFunc("ReqSendWishBeg", ReqSendWishBeg) // 发送心愿单请求
|
||||
// 无尽礼包
|
||||
RegisterMsgProcessFunc("ReqEndless", ReqEndless) // 请求无尽礼包数据
|
||||
RegisterMsgProcessFunc("ReqEndlessReward", ReqEndlessReward) // 领取无尽礼包免费奖励
|
||||
RegisterNewMsgProcessFunc("ReqEndless", ReqEndless) // 请求无尽礼包数据
|
||||
RegisterNewMsgProcessFunc("ReqEndlessReward", ReqEndlessReward) // 领取无尽礼包免费奖励
|
||||
|
||||
// 小猪存钱罐
|
||||
RegisterMsgProcessFunc("ReqPiggyBankReward", ReqPiggyBankReward) // 小猪存钱罐领取奖励
|
||||
RegisterNewMsgProcessFunc("ReqPiggyBankReward", ReqPiggyBankReward) // 小猪存钱罐领取奖励
|
||||
|
||||
// 锦标赛
|
||||
RegisterMsgProcessFunc("ReqChampship", ReqChampship) // 请求锦标赛数据
|
||||
RegisterMsgProcessFunc("ReqChampshipReward", ReqChampshipReward) // 领取锦标赛奖励
|
||||
RegisterMsgProcessFunc("ReqChampshipRankReward", ReqChampshipRankReward) // 领取锦标赛排行榜奖励
|
||||
RegisterMsgProcessFunc("ReqChampshipRank", ReqChampshipRank) // 请求锦标赛排行榜
|
||||
RegisterMsgProcessFunc("ReqChampshipPreRank", ReqChampshipPreRank) // 请求锦标赛昨日排行榜
|
||||
RegisterNewMsgProcessFunc("ReqChampship", ReqChampship) // 请求锦标赛数据
|
||||
RegisterNewMsgProcessFunc("ReqChampshipReward", ReqChampshipReward) // 领取锦标赛奖励
|
||||
RegisterNewMsgProcessFunc("ReqChampshipRankReward", ReqChampshipRankReward) // 领取锦标赛排行榜奖励
|
||||
RegisterNewMsgProcessFunc("ReqChampshipRank", ReqChampshipRank) // 请求锦标赛排行榜
|
||||
RegisterNewMsgProcessFunc("ReqChampshipPreRank", ReqChampshipPreRank) // 请求锦标赛昨日排行榜
|
||||
|
||||
// #region 活动
|
||||
RegisterMsgProcessFunc("ReqActivityReward", ReqActivityReward) // 领取活动奖励
|
||||
RegisterMsgProcessFunc("ReqAddGiftReward", ReqAddGiftReward) // 领取加赠活动奖励
|
||||
RegisterNewMsgProcessFunc("ReqActivityReward", ReqActivityReward) // 领取活动奖励
|
||||
RegisterNewMsgProcessFunc("ReqAddGiftReward", ReqAddGiftReward) // 领取加赠活动奖励
|
||||
// 挖矿
|
||||
RegisterMsgProcessFunc("ReqMining", ReqMining) // 请求挖矿数据
|
||||
RegisterMsgProcessFunc("ReqMiningReward", ReqMiningReward) // 领取挖矿奖励
|
||||
RegisterMsgProcessFunc("ReqMiningTake", ReqMiningTake) // 挖矿
|
||||
RegisterNewMsgProcessFunc("ReqMining", ReqMining) // 请求挖矿数据
|
||||
RegisterNewMsgProcessFunc("ReqMiningReward", ReqMiningReward) // 领取挖矿奖励
|
||||
RegisterNewMsgProcessFunc("ReqMiningTake", ReqMiningTake) // 挖矿
|
||||
// 猜颜色
|
||||
RegisterMsgProcessFunc("ReqGuessColor", ReqGuessColor) // 请求猜颜色数据
|
||||
RegisterMsgProcessFunc("ReqGuessColorReward", ReqGuessColorReward) // 领取猜颜色奖励
|
||||
RegisterMsgProcessFunc("ReqGuessColorTake", ReqGuessColorTake) // 猜颜色
|
||||
RegisterNewMsgProcessFunc("ReqGuessColor", ReqGuessColor) // 请求猜颜色数据
|
||||
RegisterNewMsgProcessFunc("ReqGuessColorReward", ReqGuessColorReward) // 领取猜颜色奖励
|
||||
RegisterNewMsgProcessFunc("ReqGuessColorTake", ReqGuessColorTake) // 猜颜色
|
||||
// 三段竞赛
|
||||
RegisterMsgProcessFunc("ReqRace", ReqRace)
|
||||
RegisterMsgProcessFunc("ReqRaceReward", ReqRaceReward)
|
||||
RegisterMsgProcessFunc("ReqRaceStart", ReqRaceStart)
|
||||
RegisterNewMsgProcessFunc("ReqRace", ReqRace)
|
||||
RegisterNewMsgProcessFunc("ReqRaceReward", ReqRaceReward)
|
||||
RegisterNewMsgProcessFunc("ReqRaceStart", ReqRaceStart)
|
||||
// 猫草大作战
|
||||
RegisterMsgProcessFunc("ReqCatnip", ReqCatnip) // 请求猫草大作战数据
|
||||
RegisterMsgProcessFunc("ReqCatnipInvite", ReqCatnipInvite) // 猫草大作战邀请好友
|
||||
RegisterMsgProcessFunc("ReqCatnipAgree", ReqCatnipAgree) // 同意邀请
|
||||
RegisterMsgProcessFunc("ReqCatnipRefuse", ReqCatnipRefuse) // 拒绝邀请
|
||||
RegisterMsgProcessFunc("ReqCatnipMultiply", ReqCatnipMultiply) // 猫草大作战倍数
|
||||
RegisterMsgProcessFunc("ReqCatnipPlay", ReqCatnipPlay) // 猫草大作战游戏转盘
|
||||
RegisterMsgProcessFunc("ReqCatnipReward", ReqCatnipReward) // 猫草大作战领取奖励
|
||||
RegisterMsgProcessFunc("ReqCatnipGrandReward", ReqCatnipGrandReward) // 猫草大作战领取大奖
|
||||
RegisterMsgProcessFunc("ReqCatnipEmoji", ReqCatnipEmoji)
|
||||
RegisterNewMsgProcessFunc("ReqCatnip", ReqCatnip) // 请求猫草大作战数据
|
||||
RegisterNewMsgProcessFunc("ReqCatnipInvite", ReqCatnipInvite) // 猫草大作战邀请好友
|
||||
RegisterNewMsgProcessFunc("ReqCatnipAgree", ReqCatnipAgree) // 同意邀请
|
||||
RegisterNewMsgProcessFunc("ReqCatnipRefuse", ReqCatnipRefuse) // 拒绝邀请
|
||||
RegisterNewMsgProcessFunc("ReqCatnipMultiply", ReqCatnipMultiply) // 猫草大作战倍数
|
||||
RegisterNewMsgProcessFunc("ReqCatnipPlay", ReqCatnipPlay) // 猫草大作战游戏转盘
|
||||
RegisterNewMsgProcessFunc("ReqCatnipReward", ReqCatnipReward) // 猫草大作战领取奖励
|
||||
RegisterNewMsgProcessFunc("ReqCatnipGrandReward", ReqCatnipGrandReward) // 猫草大作战领取大奖
|
||||
RegisterNewMsgProcessFunc("ReqCatnipEmoji", ReqCatnipEmoji)
|
||||
// 猫猫回礼
|
||||
RegisterNewMsgProcessFunc("ReqCatReturnGift", ReqCatReturnGift) // 请求猫猫回礼数据
|
||||
RegisterNewMsgProcessFunc("ReqCatReturnGiftReward", ReqCatReturnGiftReward) // 领取猫猫回礼奖励
|
||||
RegisterNewMsgProcessFunc("ReqCatReturnGiftScore", ReqCatReturnGiftScore) // 领取猫猫回礼积分
|
||||
RegisterNewMsgProcessFunc("ReqCatReturnGiftRewardGift", ReqCatReturnGiftRewardGift) // 领取猫猫回礼奖励礼物
|
||||
// 活动通行证
|
||||
RegisterMsgProcessFunc("ReqActPass", ReqActPass) // 请求活动通行证数据
|
||||
RegisterMsgProcessFunc("ReqActPassReward", ReqActPassReward) // 领取活动通行证奖励
|
||||
RegisterNewMsgProcessFunc("ReqActPass", ReqActPass) // 请求活动通行证数据
|
||||
RegisterNewMsgProcessFunc("ReqActPassReward", ReqActPassReward) // 领取活动通行证奖励
|
||||
// #region playroom
|
||||
RegisterMsgProcessFunc("ReqPlayroom", ReqPlayroom) // 请求playroom数据
|
||||
RegisterMsgProcessFunc("ReqPlayroomInfo", ReqPlayroomInfo) // 请求playroom拜访信息
|
||||
RegisterMsgProcessFunc("ReqPlayroomDressSet", ReqPlayroomDressSet) // 设置服装
|
||||
RegisterMsgProcessFunc("ReqPlayroomPetAirSet", ReqPlayroomPetAirSet) // 获取宠物空气
|
||||
RegisterMsgProcessFunc("ReqPlayroomGame", ReqPlayroomGame) // 游戏结果
|
||||
RegisterMsgProcessFunc("ReqPlayroomInteract", ReqPlayroomInteract) // 宠物交互
|
||||
RegisterMsgProcessFunc("ReqPlayroomSetRoom", ReqPlayroomSetRoom) // playroom装饰
|
||||
RegisterMsgProcessFunc("ReqPlayroomSelectReward", ReqPlayroomSelectReward) // playroom选择奖励
|
||||
RegisterMsgProcessFunc("ReqPlayroomLose", ReqPlayroomLose) // 处理偷取的棋子
|
||||
RegisterMsgProcessFunc("ReqPlayroomWork", ReqPlayroomWork) // 宠物工作
|
||||
RegisterMsgProcessFunc("ReqPlayroomRest", ReqPlayroomRest) // 宠物休息
|
||||
RegisterMsgProcessFunc("ReqPlayroomDraw", ReqPlayroomDraw) // 转盘
|
||||
RegisterMsgProcessFunc("ReqPlayroomFlip", ReqPlayroomFlip) // 翻牌
|
||||
RegisterMsgProcessFunc("ReqPlayroomFlipReward", ReqPlayroomFlipReward) // 翻牌奖励
|
||||
RegisterMsgProcessFunc("ReqPlayroomChip", ReqPlayroomChip) // 消除碎片
|
||||
RegisterMsgProcessFunc("ReqPlayroomOutline", ReqPlayroomOutline) // 打工离线
|
||||
RegisterMsgProcessFunc("ReqPlayroomWrokOutline", ReqPlayroomWrokOutline) // 打工离线完成
|
||||
RegisterMsgProcessFunc("ReqPlayroomShop", ReqPlayroomShop) // playroom 商店
|
||||
RegisterMsgProcessFunc("ReqPlayroomBuyItem", ReqPlayroomBuyItem) // 购买playroom物品
|
||||
RegisterMsgProcessFunc("ReqPlayroomUpvote", ReqPlayroomUpvote) // 点赞别人的playroom
|
||||
RegisterMsgProcessFunc("ReqPlayroomUnlock", ReqPlayroomUnlock) // 解锁房间
|
||||
RegisterMsgProcessFunc("ReqPlayroomTask", ReqPlayroomTask) // playroom任务
|
||||
RegisterMsgProcessFunc("ReqPlayroomTaskReward", ReqPlayroomTaskReward) // 领取任务奖励
|
||||
RegisterMsgProcessFunc("ReqPlayroomGameShowReward", ReqPlayroomGameShowReward) // 展示游戏结果数据
|
||||
RegisterMsgProcessFunc("ReqPlayroomGuide", ReqPlayroomGuide) // 展示游戏结果数据
|
||||
RegisterMsgProcessFunc("ReqPetFur", ReqPetFur) // 宠物毛皮信息
|
||||
RegisterMsgProcessFunc("ReqPetFurBuy", ReqPetFurBuy) // 宠物毛皮商店购买
|
||||
RegisterMsgProcessFunc("ReqFurSet", ReqFurSet) // 宠物毛皮设置
|
||||
RegisterNewMsgProcessFunc("ReqPlayroom", ReqPlayroom) // 请求playroom数据
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomInfo", ReqPlayroomInfo) // 请求playroom拜访信息
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomDressSet", ReqPlayroomDressSet) // 设置服装
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomPetAirSet", ReqPlayroomPetAirSet) // 获取宠物空气
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomGame", ReqPlayroomGame) // 游戏结果
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomInteract", ReqPlayroomInteract) // 宠物交互
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomSetRoom", ReqPlayroomSetRoom) // playroom装饰
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomSelectReward", ReqPlayroomSelectReward) // playroom选择奖励
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomLose", ReqPlayroomLose) // 处理偷取的棋子
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomWork", ReqPlayroomWork) // 宠物工作
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomRest", ReqPlayroomRest) // 宠物休息
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomDraw", ReqPlayroomDraw) // 转盘
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomFlip", ReqPlayroomFlip) // 翻牌
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomFlipReward", ReqPlayroomFlipReward) // 翻牌奖励
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomChip", ReqPlayroomChip) // 消除碎片
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomOutline", ReqPlayroomOutline) // 打工离线
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomWorkOutline", ReqPlayroomWorkOutline) // 打工离线完成
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomShop", ReqPlayroomShop) // playroom 商店
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomBuyItem", ReqPlayroomBuyItem) // 购买playroom物品
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomUpvote", ReqPlayroomUpvote) // 点赞别人的playroom
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomUnlock", ReqPlayroomUnlock) // 解锁房间
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomTask", ReqPlayroomTask) // playroom任务
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomTaskReward", ReqPlayroomTaskReward) // 领取任务奖励
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomGameShowReward", ReqPlayroomGameShowReward) // 展示游戏结果数据
|
||||
RegisterNewMsgProcessFunc("ReqPlayroomGuide", ReqPlayroomGuide) // 展示游戏结果数据
|
||||
RegisterNewMsgProcessFunc("ReqPetFur", ReqPetFur) // 宠物毛皮信息
|
||||
RegisterNewMsgProcessFunc("ReqPetFurBuy", ReqPetFurBuy) // 宠物毛皮商店购买
|
||||
RegisterNewMsgProcessFunc("ReqFurSet", ReqFurSet) // 宠物毛皮设置
|
||||
// 宠物宝藏
|
||||
RegisterMsgProcessFunc("ReqFriendTreasure", ReqFriendTreasure) // 请求好友宝藏数据
|
||||
RegisterMsgProcessFunc("ReqFriendTreasureStart", ReqFriendTreasureStart) // 开始游戏
|
||||
RegisterMsgProcessFunc("ReqFriendTreasureFilp", ReqFriendTreasureFilp) // 翻牌
|
||||
RegisterMsgProcessFunc("ReqFriendTreasureEnd", ReqFriendTreasureEnd) // 结束游戏
|
||||
RegisterNewMsgProcessFunc("ReqFriendTreasure", ReqFriendTreasure) // 请求好友宝藏数据
|
||||
RegisterNewMsgProcessFunc("ReqFriendTreasureStart", ReqFriendTreasureStart) // 开始游戏
|
||||
RegisterNewMsgProcessFunc("ReqFriendTreasureFilp", ReqFriendTreasureFilp) // 翻牌
|
||||
RegisterNewMsgProcessFunc("ReqFriendTreasureEnd", ReqFriendTreasureEnd) // 结束游戏
|
||||
|
||||
// #region 充值
|
||||
RegisterMsgProcessFunc("ReqCreateOrderSn", ReqCreateOrderSn) // 创建订单号
|
||||
RegisterMsgProcessFunc("ReqShippingOrder", ReqShippingOrder) // 获取订单号
|
||||
RegisterMsgProcessFunc("ReqChargeReceive", ReqChargeReceive) // 礼包回复邮件
|
||||
RegisterNewMsgProcessFunc("ReqCreateOrderSn", ReqCreateOrderSn) // 创建订单号
|
||||
RegisterNewMsgProcessFunc("ReqShippingOrder", ReqShippingOrder) // 获取订单号
|
||||
RegisterNewMsgProcessFunc("ReqChargeReceive", ReqChargeReceive) // 礼包回复邮件
|
||||
}
|
||||
|
||||
func (ad *GameLogic) InitActivity() {
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
activityCfg "server/conf/activity"
|
||||
catnipCfg "server/conf/catnip"
|
||||
champshipCfg "server/conf/champship"
|
||||
dailyTaskCfg "server/conf/daily_task"
|
||||
guesscolorCfg "server/conf/guess_color"
|
||||
languageCfg "server/conf/language"
|
||||
@ -17,7 +16,9 @@ import (
|
||||
"server/game/mod/mail"
|
||||
GoUtil "server/game_util"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
// 活动模块 登录
|
||||
@ -99,6 +100,8 @@ func (p *Player) ActivityLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
// 猫猫回礼登录
|
||||
p.CatReturnGiftLogin()
|
||||
}
|
||||
|
||||
// 发送活动邮件
|
||||
@ -133,10 +136,35 @@ func (p *Player) SendActivityMail(ItemId, ItemNum, ActivityId int, RewardItems [
|
||||
})
|
||||
}
|
||||
|
||||
// 发送活动邮件
|
||||
func (p *Player) SendActivityMail2(items []*item.Item, mail_title, mail_content string) {
|
||||
MailMod := p.PlayMod.getMailMod()
|
||||
mt_zh := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_CN, mail_title)
|
||||
mc_zh := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_CN, mail_content)
|
||||
mt_en := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_EN, mail_title)
|
||||
mc_en := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_EN, mail_content)
|
||||
mt_pt := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_PTBR, mail_title)
|
||||
mc_pt := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_PTBR, mail_content)
|
||||
mt_es := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_ES_LATAM, mail_title)
|
||||
mc_es := languageCfg.GetLanguage(msg.LANG_TYPE_LANG_ES_LATAM, mail_content)
|
||||
MailMod.SendMail(&mail.MailStruct{
|
||||
Title: mt_zh,
|
||||
Content: mc_zh,
|
||||
TitleEn: mt_en,
|
||||
ContentEn: mc_en,
|
||||
TitlePtBr: mt_pt,
|
||||
ContentPtBr: mc_pt,
|
||||
TitleEsLatam: mt_es,
|
||||
ContentEsLatam: mc_es,
|
||||
Items: items,
|
||||
Type: mail.MAIL_TYPE_NORMAL,
|
||||
})
|
||||
}
|
||||
|
||||
// 活动模块 零点更新
|
||||
func (p *Player) ActivityZeroUpdate() {
|
||||
p.CatReturnGiftZeroUpdate()
|
||||
p.ActivityLogin()
|
||||
|
||||
type zeroHandler struct {
|
||||
actType int
|
||||
updateFn func(int)
|
||||
@ -154,6 +182,43 @@ func (p *Player) ActivityZeroUpdate() {
|
||||
}
|
||||
}
|
||||
}
|
||||
func (p *Player) CatReturnGiftLogin() {
|
||||
activityInfo := p.GetActivityInfo(activity.ACT_TYPE_CAT_RETURN_GIFT)
|
||||
var aid int
|
||||
var id int
|
||||
if activityInfo != nil {
|
||||
id = activityInfo.Id
|
||||
aid = activityInfo.AId
|
||||
}
|
||||
p.GetCatReturnGiftMod().Login(id, aid)
|
||||
}
|
||||
|
||||
// 猫猫回礼0点更新
|
||||
func (p *Player) CatReturnGiftZeroUpdate() {
|
||||
activityInfo := p.GetActivityInfo(activity.ACT_TYPE_CAT_RETURN_GIFT)
|
||||
var aid int
|
||||
var id int
|
||||
if activityInfo != nil {
|
||||
id = activityInfo.Id
|
||||
aid = activityInfo.AId
|
||||
}
|
||||
oldId, oldScore, oldReward := p.GetCatReturnGiftMod().ZeroUpdate(id, aid)
|
||||
if oldId != 0 {
|
||||
log.Debug("CatReturnGiftZeroUpdate oldId : %d, oldScore : %d, oldReward : %d", oldId, oldScore, oldReward)
|
||||
cfg := G_GameLogicPtr.ActivityMgr.GetCatReturnGiftCfg(oldId)
|
||||
items := make([]*item.Item, 0)
|
||||
if cfg != nil {
|
||||
for _, v := range cfg.RewardList {
|
||||
if oldScore >= int(v.Score) && oldReward < int(v.Id) {
|
||||
items = append(items, item.MsgToItem(v.Reward)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(items) > 0 {
|
||||
p.SendActivityMail2(items, "backend_milestone_mail_title", "backend_milestone_mail_content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取活动信息
|
||||
func (p *Player) GetActivityInfo(actType int) *ActivityInfo {
|
||||
@ -468,11 +533,14 @@ func (p *Player) GetChampshipActivityId() (int, int) {
|
||||
var yesterdayActivityId int
|
||||
activiyCfgList := G_GameLogicPtr.ActivityMgr.GetActivityList()
|
||||
now := GoUtil.Now()
|
||||
yesterday := GoUtil.ZeroTimestamp() - 1
|
||||
level := p.GetBaseMod().GetLevel()
|
||||
champshipActivityIds := champshipCfg.GetChampshipActivityId()
|
||||
type sortData struct {
|
||||
Id int
|
||||
EndTime int64
|
||||
}
|
||||
var sortList []sortData
|
||||
for _, v := range activiyCfgList {
|
||||
if !GoUtil.InArray(v.Id, champshipActivityIds) {
|
||||
if v.Type != activity.ACT_TYPE_CHAMPION {
|
||||
continue
|
||||
}
|
||||
if v.Level > level {
|
||||
@ -481,13 +549,92 @@ func (p *Player) GetChampshipActivityId() (int, int) {
|
||||
if v.Startime <= now && v.Endtime >= now {
|
||||
todayActivityId = v.Id
|
||||
}
|
||||
if v.Startime <= yesterday && v.Endtime >= yesterday {
|
||||
yesterdayActivityId = v.Id
|
||||
if v.Endtime < now {
|
||||
sortList = append(sortList, sortData{Id: v.Id, EndTime: v.Endtime})
|
||||
}
|
||||
}
|
||||
if len(sortList) > 0 {
|
||||
sort.Slice(sortList, func(i, j int) bool {
|
||||
return sortList[i].EndTime > sortList[j].EndTime
|
||||
})
|
||||
yesterdayActivityId = sortList[0].Id
|
||||
}
|
||||
return todayActivityId, yesterdayActivityId
|
||||
}
|
||||
|
||||
func (p *Player) ChampionshipZeroUpdate() {
|
||||
todayActivityId, _ := p.GetChampshipActivityId()
|
||||
ChampionshipMod := p.PlayMod.getChampshipMod()
|
||||
aid := ChampionshipMod.AId
|
||||
var items []*item.Item
|
||||
items = p.GetChampshipReward(aid)
|
||||
if len(items) > 0 {
|
||||
p.SendActivityMail2(items, "backend_championship_mail_title", "backend_championship_mail_content")
|
||||
p.PushClientRes(p.GetMailMod().BackData())
|
||||
}
|
||||
p.PlayMod.getChampshipMod().ZeroUpdate(todayActivityId)
|
||||
}
|
||||
|
||||
func (p *Player) GetChampshipReward(id int) []*item.Item {
|
||||
ChampionshipMod := p.PlayMod.getChampshipMod()
|
||||
if id == 0 { // 兼容旧数据,之前没有活动id的只要发放一次奖励
|
||||
DecorateMod := p.PlayMod.getDecorateMod()
|
||||
orderFactor := orderCfg.GetOrderFactor(DecorateMod.GetAreaId())
|
||||
return ChampionshipMod.GetReward(0, orderFactor)
|
||||
}
|
||||
if ChampionshipMod == nil {
|
||||
return nil
|
||||
}
|
||||
cfg := G_GameLogicPtr.ActivityMgr.GetChampshipCfg(id)
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
DecorateMod := p.PlayMod.getDecorateMod()
|
||||
orderFactor := orderCfg.GetOrderFactor(DecorateMod.GetAreaId())
|
||||
var maxRewardId int
|
||||
var items []*item.Item
|
||||
for _, v := range cfg.GetJackpotList() {
|
||||
if ChampionshipMod.GetScore() >= int(v.Total) && int(v.Id) > ChampionshipMod.GetRewardId() {
|
||||
maxRewardId = int(v.Id)
|
||||
items = item.Merge(items, item.MsgToItem(v.Items))
|
||||
if v.StarReward > 0 {
|
||||
itemNum := GoUtil.FormatStarItemNum(int(v.StarReward), orderFactor)
|
||||
starItem := item.NewItem(item.ITEM_STAR_ID, itemNum)
|
||||
items = item.Merge(items, []*item.Item{starItem})
|
||||
}
|
||||
}
|
||||
}
|
||||
ChampionshipMod.SetRewardId(maxRewardId)
|
||||
return items
|
||||
}
|
||||
|
||||
func (p *Player) GetChampshipRankReward(rank, aid int) ([]*item.Item, error) {
|
||||
ChampionshipMod := p.PlayMod.getChampshipMod()
|
||||
if aid == 0 { // 兼容旧数据,之前没有活动id的只要发放一次奖励
|
||||
return ChampionshipMod.GetRankReward(rank, aid)
|
||||
}
|
||||
if ChampionshipMod == nil {
|
||||
return nil, fmt.Errorf("championship mod is nil")
|
||||
}
|
||||
if ChampionshipMod.HasRankReward() {
|
||||
return nil, fmt.Errorf("rank reward has been received")
|
||||
}
|
||||
cfg := G_GameLogicPtr.ActivityMgr.GetChampshipCfg(aid)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("championship config is nil")
|
||||
}
|
||||
ChampionshipMod.SetRankReward()
|
||||
rankRewardList := cfg.GetRankList()
|
||||
var items []*item.Item
|
||||
for _, v := range rankRewardList {
|
||||
if rank <= int(v.Max) && rank >= int(v.Min) {
|
||||
items = item.Merge(items, item.MsgToItem(v.Items))
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no rank reward found for rank %d", rank)
|
||||
}
|
||||
|
||||
func (p *Player) GetDailyTaskActivityId() int {
|
||||
var activityId int
|
||||
activiyCfgList := G_GameLogicPtr.ActivityMgr.GetActivityList()
|
||||
@ -508,3 +655,24 @@ func (p *Player) GetDailyTaskActivityId() int {
|
||||
}
|
||||
return activityId
|
||||
}
|
||||
|
||||
// 猫猫回礼返回
|
||||
func (p *Player) CatReturnGiftBackData() {
|
||||
activityInfo := p.GetActivityInfo(activity.ACT_TYPE_CAT_RETURN_GIFT)
|
||||
if activityInfo == nil {
|
||||
return
|
||||
}
|
||||
cfg := G_GameLogicPtr.ActivityMgr.GetCatReturnGiftCfg(activityInfo.Id)
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
CatReturnGiftMod := p.GetCatReturnGiftMod()
|
||||
res := &msg.ResCatReturnGift{
|
||||
StartTime: activityInfo.StartT,
|
||||
EndTime: activityInfo.EndT,
|
||||
Cfg: cfg,
|
||||
Score: int32(CatReturnGiftMod.GetScore()),
|
||||
Reward: int32(CatReturnGiftMod.GetReward()),
|
||||
}
|
||||
p.PushClientRes(res)
|
||||
}
|
||||
|
||||
@ -3,13 +3,16 @@ package game
|
||||
import (
|
||||
"fmt"
|
||||
"server/db"
|
||||
"server/game/mod/activity"
|
||||
"server/game/mod/msg"
|
||||
Msg "server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
protoMsg "server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
type ActivityMgr struct {
|
||||
@ -23,6 +26,7 @@ type ActivityData struct {
|
||||
|
||||
type ActivityCfg struct {
|
||||
Id int
|
||||
AId int
|
||||
Type int
|
||||
Startime int64
|
||||
Endtime int64
|
||||
@ -31,6 +35,7 @@ type ActivityCfg struct {
|
||||
MailTitle string
|
||||
MailContent string
|
||||
cfg interface{}
|
||||
Interval int64
|
||||
Extra map[string]interface{}
|
||||
}
|
||||
|
||||
@ -63,7 +68,24 @@ func (r *ActivityMgr) GetActivityList() []ActivityCfg {
|
||||
data.mu.Lock()
|
||||
defer data.mu.Unlock()
|
||||
list := make([]ActivityCfg, 0, len(data.List))
|
||||
now := GoUtil.Now()
|
||||
for _, v := range data.List {
|
||||
//循环活动,重新计算活动时间
|
||||
if v.Interval > 0 {
|
||||
if now > v.Endtime {
|
||||
//活动已结束,计算下一次活动时间
|
||||
interval := (now - v.Startime) / v.Interval
|
||||
v.Startime += interval * v.Interval
|
||||
v.Endtime += interval * v.Interval
|
||||
if now > v.Endtime {
|
||||
v.Startime += v.Interval
|
||||
v.Endtime += v.Interval
|
||||
}
|
||||
}
|
||||
v.AId = int(v.Startime) //活动id用开始时间表示,方便客户端排序
|
||||
} else {
|
||||
v.AId = v.Id
|
||||
}
|
||||
list = append(list, *v)
|
||||
}
|
||||
return list
|
||||
@ -111,10 +133,17 @@ func (r *ActivityMgr) Reload() error {
|
||||
MailTitle: v.MailTitle,
|
||||
MailContent: v.MailContent,
|
||||
cfg: activityCfg,
|
||||
Interval: v.Interval,
|
||||
}
|
||||
log.Debug("load activity cfg: %v", cfg)
|
||||
data.List[v.Id] = cfg
|
||||
}
|
||||
go func() {
|
||||
actList := r.GetActivityList()
|
||||
for _, v := range actList {
|
||||
log.Debug("activity load success: type :%d, id :%d, startTime :%s, endTime :%s", v.Type, v.Id, GoUtil.FormatTime(v.Startime), GoUtil.FormatTime(v.Endtime))
|
||||
}
|
||||
}()
|
||||
G_GameLogicPtr.NotifyAll(&Msg.Msg{Type: Msg.HANDLE_TYPE_ACTIVITY_RELOAD})
|
||||
return nil
|
||||
}
|
||||
@ -126,7 +155,21 @@ func unmarshalActivityCfg(atype int, buf []byte) (interface{}, error) {
|
||||
switch atype {
|
||||
case 1:
|
||||
cfg := &protoMsg.MiningCfg{}
|
||||
err := proto.Unmarshal(buf, cfg)
|
||||
err := protojson.Unmarshal(buf, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
case 9:
|
||||
cfg := &protoMsg.ChampionshipCfg{}
|
||||
err := protojson.Unmarshal(buf, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
case 10:
|
||||
cfg := &protoMsg.CatReturnGiftCfg{}
|
||||
err := protojson.Unmarshal(buf, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -134,3 +177,32 @@ func unmarshalActivityCfg(atype int, buf []byte) (interface{}, error) {
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *ActivityMgr) GetCatReturnGiftCfg(id int) *protoMsg.CatReturnGiftCfg {
|
||||
data := r.getData()
|
||||
data.mu.Lock()
|
||||
defer data.mu.Unlock()
|
||||
for _, v := range data.List {
|
||||
if v.Type == activity.ACT_TYPE_CAT_RETURN_GIFT && v.Id == id {
|
||||
if cfg, ok := v.cfg.(*protoMsg.CatReturnGiftCfg); ok {
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ActivityMgr) GetChampshipCfg(id int) *protoMsg.ChampionshipCfg {
|
||||
data := r.getData()
|
||||
data.mu.Lock()
|
||||
defer data.mu.Unlock()
|
||||
for _, v := range data.List {
|
||||
if v.Type == activity.ACT_TYPE_CHAMPION && v.Id == id {
|
||||
if cfg, ok := v.cfg.(*protoMsg.ChampionshipCfg); ok {
|
||||
cfg.Title = v.Title
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -11,10 +11,12 @@ import (
|
||||
GoUtil "server/game_util"
|
||||
"server/gamedata"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/gate"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/gate"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@ -348,3 +350,93 @@ func AdminShipping(req *msg.ReqOrderShipping) (*msg.ResOrderShipping, error) {
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func AdminPlayerDetailInfo(req *msg.UserDetailParam) (*msg.ResUserDetail, error) {
|
||||
player := G_GameLogicPtr.GetPlayer(req.Uid)
|
||||
online := true
|
||||
if player == nil {
|
||||
player = new(Player)
|
||||
player.M_DwUin = req.Uid
|
||||
player.InitPlayerOnly()
|
||||
player.ZeroUpdate(nil)
|
||||
online = false
|
||||
}
|
||||
banTime := db.GetPlayerBan(player.PlayMod.getBaseMod().Account)
|
||||
actLog := make([]*msg.ActLog, 0, len(player.PlayMod.getFriendMod().ActivityLog))
|
||||
for _, v := range player.PlayMod.getFriendMod().ActivityLog {
|
||||
actLog = append(actLog, &msg.ActLog{
|
||||
Type: int32(v.Type),
|
||||
Time: v.Time,
|
||||
Param: v.Param,
|
||||
})
|
||||
}
|
||||
info := &msg.ResUserDetailInfo{
|
||||
Name: player.PlayMod.getBaseMod().Account,
|
||||
Uid: player.M_DwUin,
|
||||
AreaId: int32(player.PlayMod.getDecorateMod().GetAreaId()),
|
||||
Face: int32(player.PlayMod.getFaceMod().SetId),
|
||||
Charge: int32(player.PlayMod.getChargeMod().Charge),
|
||||
MaxCharge: int32(player.PlayMod.getChargeMod().MaxCharge),
|
||||
Level: int32(player.GetBaseMod().GetLevel()),
|
||||
Diamond: int64(player.GetBaseMod().GetDiamond()),
|
||||
Star: int32(player.GetBaseMod().GetStar()),
|
||||
Energy: int32(player.GetBaseMod().GetEnergy()),
|
||||
Mac: player.GetBaseMod().DiviceId,
|
||||
Login: int64(player.GetBaseMod().LoginTime),
|
||||
Cumulative: int64(player.PlayMod.getBaseMod().Cumulative),
|
||||
RegisterTime: player.GetPlayerBaseMod().GetRegisterTime(),
|
||||
TodayCumulative: int64(player.PlayMod.getBaseMod().TodayCumulative),
|
||||
Ban: banTime > GoUtil.Now() || banTime == -1,
|
||||
Bonus: int32(player.PlayMod.getLimitedTimeEventMod().Progress),
|
||||
Code: player.PlayMod.getBaseMod().AddCode,
|
||||
ActLog: actLog,
|
||||
AdWatch: int32(player.PlayMod.getKvMod().GetAdValue()),
|
||||
ChessMap: player.PlayMod.getChessMod().ChessMap,
|
||||
}
|
||||
|
||||
if online {
|
||||
info.Cumulative = int64(player.PlayMod.getBaseMod().Cumulative) + GoUtil.Now() - int64(player.PlayMod.getBaseMod().LoginTime)
|
||||
info.TodayCumulative = int64(player.PlayMod.getBaseMod().TodayCumulative) + GoUtil.Now() - int64(player.PlayMod.getBaseMod().LoginTime)
|
||||
}
|
||||
|
||||
friendList := player.PlayMod.getFriendMod().NewFriendList
|
||||
info.FriendList = make([]*msg.UserDetailFriendInfo, 0, len(friendList))
|
||||
for uid := range friendList {
|
||||
ps := G_GameLogicPtr.GetSimplePlayerByUid(uid)
|
||||
if ps == nil {
|
||||
continue
|
||||
}
|
||||
info.FriendList = append(info.FriendList, &msg.UserDetailFriendInfo{
|
||||
Uid: int64(uid),
|
||||
NickName: ps.Name,
|
||||
Avatar: int32(ps.Face),
|
||||
Level: int32(ps.Level),
|
||||
LogoutTime: ps.Loginout,
|
||||
LoginTime: ps.Login,
|
||||
})
|
||||
}
|
||||
|
||||
orderList := player.PlayMod.getOrderMod().OrderList
|
||||
info.Order = make([]*msg.UserDetailOrderInfo, 0, len(orderList))
|
||||
for orderID, order := range orderList {
|
||||
chessArr := make([]*msg.UserDetailOrderInfoChess, 0, len(order.MergeId))
|
||||
for _, chessID := range order.MergeId {
|
||||
chessArr = append(chessArr, &msg.UserDetailOrderInfoChess{
|
||||
Id: int32(chessID),
|
||||
})
|
||||
}
|
||||
info.Order = append(info.Order, &msg.UserDetailOrderInfo{
|
||||
Id: fmt.Sprint(orderID),
|
||||
Type: int32(order.Type),
|
||||
Time: order.Timestamp,
|
||||
Chess: chessArr,
|
||||
Diff: int32(order.Diff),
|
||||
})
|
||||
}
|
||||
|
||||
return &msg.ResUserDetail{
|
||||
Code: 0,
|
||||
Msg: "ok",
|
||||
Info: info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -14,10 +14,11 @@ import (
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
proto "server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -40,6 +41,7 @@ type ChampshipData struct {
|
||||
Robot map[int]*ChampshipRobot // 机器人
|
||||
PreRobot map[int]*ChampshipRobot // 机器人 备份
|
||||
ZeroTime int64
|
||||
Version int
|
||||
}
|
||||
|
||||
type ChampshipRank struct {
|
||||
@ -47,6 +49,7 @@ type ChampshipRank struct {
|
||||
Score float64
|
||||
Time int64
|
||||
Type int
|
||||
Rank int
|
||||
}
|
||||
|
||||
type ChampshipRobot struct {
|
||||
@ -95,6 +98,7 @@ func (c *ChampshipMgr) Init() {
|
||||
}
|
||||
// 注册处理函数
|
||||
c.init()
|
||||
c.version()
|
||||
now := GoUtil.Now()
|
||||
zeroTime := GoUtil.ZeroTimestamp()
|
||||
if c.getData().ZeroTime != zeroTime {
|
||||
@ -116,6 +120,44 @@ func (c *ChampshipMgr) Init() {
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ChampshipMgr) version() {
|
||||
if c.data.(*ChampshipData).Version == 0 {
|
||||
c.data.(*ChampshipData).Version = 1
|
||||
for _, v := range c.data.(*ChampshipData).Rank {
|
||||
sortChampionshipRank(v)
|
||||
for _, info := range v {
|
||||
if info.Type != RANK_PLAYER_ROBOT {
|
||||
c.SetRankCache(info.Uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, v := range c.data.(*ChampshipData).PreRank {
|
||||
sortChampionshipRank(v)
|
||||
for _, info := range v {
|
||||
if info.Type != RANK_PLAYER_ROBOT {
|
||||
c.SetRankCache(info.Uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sortChampionshipRank(rankList []*ChampshipRank) {
|
||||
rank := 1
|
||||
lastRank := 1
|
||||
lastScore := 0.0
|
||||
for _, v := range rankList {
|
||||
if v.Score == lastScore {
|
||||
v.Rank = lastRank
|
||||
} else {
|
||||
v.Rank = rank
|
||||
lastRank = rank
|
||||
lastScore = v.Score
|
||||
}
|
||||
rank++
|
||||
}
|
||||
}
|
||||
|
||||
// 每天零点30分通知所有在线玩家领取奖励
|
||||
func (c *ChampshipMgr) ZeroNotifyAll() (interface{}, error) {
|
||||
NotifyAllPlayerMsg(&msg.Msg{
|
||||
@ -229,6 +271,7 @@ func (c *ChampshipMgr) ai() (interface{}, error) {
|
||||
Robot.Time = now
|
||||
}
|
||||
r.Score += AddScore
|
||||
r.Score = math.Round(r.Score)
|
||||
} else {
|
||||
notify[r.Uid] = e
|
||||
}
|
||||
@ -241,6 +284,7 @@ func (c *ChampshipMgr) ai() (interface{}, error) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
sortChampionshipRank(v)
|
||||
for e, r := range v {
|
||||
if r.Type == RANK_PLAYER_ROBOT {
|
||||
continue
|
||||
@ -283,7 +327,7 @@ func (c *ChampshipMgr) GetPreRankMsg(uid int) *proto.ResChampshipPreRank {
|
||||
RL := make(map[int32]*proto.ResPlayerRank, 0)
|
||||
for k, v := range RankList {
|
||||
if v.Uid == uid {
|
||||
myRank = k + 1
|
||||
myRank = v.Rank
|
||||
myScore = v.Score
|
||||
}
|
||||
if v.Type == RANK_PLAYER_ROBOT {
|
||||
@ -312,6 +356,7 @@ func (c *ChampshipMgr) GetPreRankMsg(uid int) *proto.ResChampshipPreRank {
|
||||
FurSet: int32(robot.FurSet),
|
||||
PetName: robot.PetName,
|
||||
Last: last,
|
||||
Rank: int32(v.Rank),
|
||||
}
|
||||
} else {
|
||||
simplePlayer := G_GameLogicPtr.GetSimplePlayerByUid(v.Uid)
|
||||
@ -339,6 +384,7 @@ func (c *ChampshipMgr) GetPreRankMsg(uid int) *proto.ResChampshipPreRank {
|
||||
FurSet: int32(simplePlayer.PetFur),
|
||||
PetName: simplePlayer.PetName,
|
||||
Last: last,
|
||||
Rank: int32(v.Rank),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -368,7 +414,7 @@ func (c *ChampshipMgr) GetRankMsg(uid int) *proto.ResChampshipRank {
|
||||
RL := make(map[int32]*proto.ResPlayerRank, 0)
|
||||
for k, v := range rankList {
|
||||
if v.Uid == uid {
|
||||
myRank = k + 1
|
||||
myRank = v.Rank
|
||||
myScore = v.Score
|
||||
}
|
||||
if v.Type == RANK_PLAYER_ROBOT {
|
||||
@ -397,6 +443,7 @@ func (c *ChampshipMgr) GetRankMsg(uid int) *proto.ResChampshipRank {
|
||||
FurSet: int32(robot.FurSet),
|
||||
PetName: robot.PetName,
|
||||
Last: last,
|
||||
Rank: int32(v.Rank),
|
||||
}
|
||||
} else {
|
||||
simplePlayer := G_GameLogicPtr.GetSimplePlayerByUid(v.Uid)
|
||||
@ -424,6 +471,7 @@ func (c *ChampshipMgr) GetRankMsg(uid int) *proto.ResChampshipRank {
|
||||
FurSet: int32(simplePlayer.PetFur),
|
||||
PetName: simplePlayer.PetName,
|
||||
Last: last,
|
||||
Rank: int32(v.Rank),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -530,7 +578,7 @@ func (c *ChampshipMgr) group(iszero bool) (interface{}, error) {
|
||||
ChampshipData.Robot[ChampshipData.RobotId] = v
|
||||
ChampshipData.Rank[j] = append(ChampshipData.Rank[j], &ChampshipRank{
|
||||
Uid: ChampshipData.RobotId,
|
||||
Score: v.Score,
|
||||
Score: math.Round(v.Score),
|
||||
Time: v.Time,
|
||||
Type: RANK_PLAYER_ROBOT,
|
||||
})
|
||||
@ -542,6 +590,7 @@ func (c *ChampshipMgr) group(iszero bool) (interface{}, error) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
sortChampionshipRank(ChampshipData.Rank[j])
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,6 +683,8 @@ func (c *ChampshipMgr) inRank(m *msg.Msg) (interface{}, error) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
// 重新排序 相同分数排名相同
|
||||
sortChampionshipRank(rankList)
|
||||
ChampshipData.Rank[groupId] = rankList
|
||||
|
||||
// 收集需要通知的玩家
|
||||
@ -668,9 +719,9 @@ func (c *ChampshipMgr) getMyRank(uid int) int {
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for k, v := range rankList {
|
||||
for _, v := range rankList {
|
||||
if v.Uid == uid {
|
||||
return k + 1
|
||||
return v.Rank
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@ -686,9 +737,9 @@ func (c *ChampshipMgr) unsafe_getMyRank(uid int) int {
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for k, v := range rankList {
|
||||
for _, v := range rankList {
|
||||
if v.Uid == uid {
|
||||
return k + 1
|
||||
return v.Rank
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@ -706,9 +757,9 @@ func (c *ChampshipMgr) getLastMyRank(uid int) int {
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for k, v := range rankList {
|
||||
for _, v := range rankList {
|
||||
if v.Uid == uid {
|
||||
return k + 1
|
||||
return v.Rank
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@ -723,9 +774,9 @@ func (c *ChampshipMgr) unsafe_getLastMyRank(uid int) int {
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for k, v := range rankList {
|
||||
for _, v := range rankList {
|
||||
if v.Uid == uid {
|
||||
return k + 1
|
||||
return v.Rank
|
||||
}
|
||||
}
|
||||
return 0
|
||||
|
||||
@ -17,9 +17,10 @@ import (
|
||||
"server/game/mod/quest"
|
||||
GoUtil "server/game_util"
|
||||
proto "server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
func (p *Player) Charge(chargeId int) {
|
||||
|
||||
@ -3,8 +3,9 @@ package game
|
||||
import (
|
||||
"reflect"
|
||||
"server/game/internal"
|
||||
"server/pkg/github.com/name5566/leaf/gate"
|
||||
"sort"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/gate"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -17,6 +18,10 @@ const (
|
||||
onehour = 3600
|
||||
oneday = 24 * onehour
|
||||
sevendays = 7 * oneday
|
||||
month = 30 * oneday
|
||||
halfyear = 6 * month
|
||||
oneyear = 365 * oneday
|
||||
tenyear = 10 * oneyear
|
||||
)
|
||||
|
||||
// 解析参数
|
||||
|
||||
@ -12,9 +12,11 @@ import (
|
||||
|
||||
"server/msg"
|
||||
|
||||
"server/pkg/github.com/name5566/leaf/gate"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/gate"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"server/db"
|
||||
@ -110,8 +112,12 @@ func HandleClientReq(args []interface{}) {
|
||||
case "ReqAdminInfo": // 后台接口
|
||||
AdminProcess(m.GetFunc(), []interface{}{a, buf})
|
||||
case "ReqLoginCode":
|
||||
Detail := &msg.ReqLoginCode{}
|
||||
proto.Unmarshal(buf, Detail)
|
||||
detailMsg, err := UnmarshalProtoMessageByName(m.GetFunc(), buf)
|
||||
if err != nil {
|
||||
log.Error("unmarshal %s failed: %v", m.GetFunc(), err)
|
||||
return
|
||||
}
|
||||
Detail := detailMsg.(*msg.ReqLoginCode)
|
||||
Code, err := GeneratedCode(Detail.TelPhone)
|
||||
ResLoginCode := &msg.ResLoginCode{}
|
||||
if err != nil {
|
||||
@ -125,8 +131,8 @@ func HandleClientReq(args []interface{}) {
|
||||
G_GameLogicPtr.SendServerVersion(a)
|
||||
case "ReqRegisterAccount":
|
||||
detail := &msg.ReqRegisterAccount{}
|
||||
log.Debug("player %s start register", detail.UserName)
|
||||
proto.Unmarshal(buf, detail)
|
||||
log.Debug("player %s start register", detail.UserName)
|
||||
gl := G_getGameLogic()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -226,7 +232,16 @@ func HandleClientReq(args []interface{}) {
|
||||
p.(*Player).args = make(map[string]interface{})
|
||||
p.(*Player).args["func"] = m
|
||||
p.(*Player).args["agent"] = a
|
||||
err := RunNetProcessByKey(m.GetFunc(), []interface{}{a, buf})
|
||||
detailMsg, err := UnmarshalProtoMessageByName(m.GetFunc(), buf)
|
||||
if err != nil {
|
||||
log.Error("uid : %d, func : %s, unmarshal error : %s", p.(*Player).M_DwUin, m.GetFunc(), err)
|
||||
p.(*Player).TeLog("func_unmarshal_error", map[string]interface{}{
|
||||
"method_name": m.GetFunc(),
|
||||
"error_info": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
err = RunNewNetProcessByKey(m.GetFunc(), p.(*Player), &detailMsg)
|
||||
if err != nil {
|
||||
log.Error("uid : %d, func : %s, err : %s", p.(*Player).M_DwUin, m.GetFunc(), err)
|
||||
p.(*Player).TeLog("func_exec_error", map[string]interface{}{
|
||||
@ -236,10 +251,16 @@ func HandleClientReq(args []interface{}) {
|
||||
p.(*Player).Recover(backup) //还原Player的数据
|
||||
return
|
||||
}
|
||||
str := ""
|
||||
if conf.Server.GameName == "pet_home" || conf.Server.GameName == "merge_pet_sdk" {
|
||||
strbuf, _ := protojson.Marshal(detailMsg)
|
||||
str = string(strbuf)
|
||||
}
|
||||
p.(*Player).ProcessTrigger()
|
||||
p.(*Player).TeLog("func_exec_time", map[string]interface{}{
|
||||
"method_name": m.GetFunc(),
|
||||
"exec_time": fmt.Sprintf("%v", time.Since(start)),
|
||||
"proto": str,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,9 @@ import (
|
||||
"server/db"
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
func (p *Player) GetVisitorPlayer() int {
|
||||
@ -34,6 +35,9 @@ func (p *Player) GetVisitorPlayer() int {
|
||||
if GoUtil.InArray(k, todayVisitedUsers) {
|
||||
continue
|
||||
}
|
||||
if GoUtil.InArray(k, PlayroomMod.RandVisitor) {
|
||||
continue
|
||||
}
|
||||
if v.Time < now-86400 {
|
||||
continue
|
||||
}
|
||||
@ -65,6 +69,9 @@ func (p *Player) GetVisitorPlayer() int {
|
||||
if GoUtil.InArray(uid, todayVisitedUsers) {
|
||||
continue
|
||||
}
|
||||
if GoUtil.InArray(uid, PlayroomMod.RandVisitor) {
|
||||
continue
|
||||
}
|
||||
ps := G_GameLogicPtr.GetSimplePlayerByUid(uid)
|
||||
if ps == nil {
|
||||
continue
|
||||
@ -301,6 +308,11 @@ func GetRecommendPlayer(p *Player, num int) []int {
|
||||
endFilterList = notChargeNotWatchAdFilterFunc(chargeFilterList)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endFilterList = make([]int, 0, len(chargeFilterList))
|
||||
for _, ps := range chargeFilterList {
|
||||
endFilterList = append(endFilterList, ps.Uid)
|
||||
}
|
||||
}
|
||||
|
||||
recommendList := GoUtil.RandSliceNum(endFilterList, num)
|
||||
|
||||
@ -6,7 +6,8 @@ import (
|
||||
"server/conf"
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
type FriendMgr struct {
|
||||
|
||||
@ -87,6 +87,7 @@ type ActivityInfo struct {
|
||||
StartT int64
|
||||
EndT int64
|
||||
Id int
|
||||
AId int
|
||||
Type int
|
||||
Title string
|
||||
Name string
|
||||
|
||||
@ -31,18 +31,15 @@ import (
|
||||
"server/game/mod/playroom"
|
||||
GoUtil "server/game_util"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
func ReqGmCommand(player *Player, buf []byte) error {
|
||||
detail := &msg.ReqGmCommand{}
|
||||
proto.Unmarshal(buf, detail)
|
||||
return ReqGmCommand_(player, detail.Command)
|
||||
func ReqGmCommand(player *Player, req *msg.ReqGmCommand) error {
|
||||
return ReqGmCommand_(player, req.Command)
|
||||
}
|
||||
func ReqGmCommand_(player *Player, Command string) error {
|
||||
// defer func() {
|
||||
@ -359,7 +356,7 @@ func ReqGmCommand_(player *Player, Command string) error {
|
||||
player.RaceBackData()
|
||||
case "playroomReset":
|
||||
PlayroomMod := playroom.PlayroomMod{}
|
||||
PlayroomMod.InitData()
|
||||
PlayroomMod.InitData(player.M_DwUin, player.PlayerBaseMod.GetRegisterTime())
|
||||
player.PlayMod.mod_list.Playroom = PlayroomMod
|
||||
case "resetCollect":
|
||||
CollectMod := player.PlayMod.getCollectMod()
|
||||
@ -531,10 +528,8 @@ func ReqGmCommand_(player *Player, Command string) error {
|
||||
BaseMod.Uid = Uid
|
||||
case "copyUser":
|
||||
p1 := new(Player)
|
||||
err := p1.InitPlayer(arg[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p1.M_DwUin = int64(GoUtil.Int(arg[1]))
|
||||
p1.InitPlayerOnly()
|
||||
BaseMod := p1.PlayMod.getBaseMod()
|
||||
BaseMod.Uid = player.M_DwUin
|
||||
BaseMod.NickName = player.PlayMod.getBaseMod().NickName
|
||||
@ -655,6 +650,11 @@ func ReqGmCommand_(player *Player, Command string) error {
|
||||
Id, _ := strconv.Atoi(arg[1])
|
||||
player.PlayMod.getInviteMod().AddInvite(Id)
|
||||
player.PushClientRes(player.PlayMod.getInviteMod().NotifySuccess())
|
||||
case "resetCatReturnGift":
|
||||
CatReturnGiftMod := player.GetCatReturnGiftMod()
|
||||
CatReturnGiftMod.AId = 0
|
||||
player.CatReturnGiftLogin()
|
||||
player.CatReturnGiftBackData()
|
||||
case "championshipResult":
|
||||
uid, _ := strconv.Atoi(arg[1])
|
||||
FriendMgrSend(&MsgMod.Msg{
|
||||
@ -664,6 +664,8 @@ func ReqGmCommand_(player *Player, Command string) error {
|
||||
From: uid,
|
||||
Extra: []int{2, 145},
|
||||
})
|
||||
case "resetKv":
|
||||
player.PlayMod.mod_list.Kv.Data = make(map[int]string)
|
||||
case "debugBeckmarkMsg":
|
||||
for i := 0; i < 1000000; i++ {
|
||||
for j := 0; j < 10; j++ {
|
||||
@ -675,9 +677,13 @@ func ReqGmCommand_(player *Player, Command string) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
case "catScore":
|
||||
score := GoUtil.Int(arg[1])
|
||||
player.GetCatReturnGiftMod().AddScore(score)
|
||||
player.CatReturnGiftBackData()
|
||||
case "debugLogoutMsg":
|
||||
ToUid, _ := strconv.Atoi(arg[1])
|
||||
uidList, err := db.GetDebugPlayer(ToUid)
|
||||
toUid, _ := strconv.Atoi(arg[1])
|
||||
uidList, err := db.GetDebugPlayer(toUid)
|
||||
if err != nil {
|
||||
log.Error("GetDebugPlayer err:%s", err.Error())
|
||||
return err
|
||||
@ -687,13 +693,13 @@ func ReqGmCommand_(player *Player, Command string) error {
|
||||
Type: MsgMod.HANDLE_TYPE_APPLY,
|
||||
SendT: GoUtil.Now(),
|
||||
From: uid,
|
||||
To: ToUid,
|
||||
To: toUid,
|
||||
})
|
||||
FriendMgrSend(&MsgMod.Msg{
|
||||
Type: MsgMod.HANDLE_TYPE_HANDBOOK_COLLECTION,
|
||||
SendT: GoUtil.Now(),
|
||||
From: uid,
|
||||
To: ToUid,
|
||||
To: toUid,
|
||||
})
|
||||
}
|
||||
default:
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
GoUtil "server/game_util"
|
||||
"sync"
|
||||
|
||||
"server/pkg/github.com/name5566/leaf/gate"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/gate"
|
||||
)
|
||||
|
||||
var Agents = sync.Map{}
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"server/base"
|
||||
GoUtil "server/game_util"
|
||||
|
||||
"server/pkg/github.com/name5566/leaf/module"
|
||||
"gitea.bywaystudios.com/pet_home/leaf/module"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@ -119,16 +119,22 @@ func (p *Player) LimitedTimeCardTrigger() {
|
||||
case card.STATUS_CARD_EX_1:
|
||||
delete(FriendMod.Card, k)
|
||||
FriendMgrSend(&MsgMod.Msg{
|
||||
From: v.BUid,
|
||||
To: v.AUid,
|
||||
Type: MsgMod.HANDLE_TYPE_EX_CARD_SELECT_TIMEOUT,
|
||||
From: v.BUid,
|
||||
To: v.AUid,
|
||||
Type: MsgMod.HANDLE_TYPE_EX_CARD_SELECT_TIMEOUT,
|
||||
SendT: GoUtil.Now(),
|
||||
End: GoUtil.Now() + tenyear,
|
||||
Extra: *v,
|
||||
})
|
||||
case card.STATUS_CARD_EX_2:
|
||||
delete(FriendMod.Card, k)
|
||||
FriendMgrSend(&MsgMod.Msg{
|
||||
From: v.AUid,
|
||||
To: v.BUid,
|
||||
Type: MsgMod.HANDLE_TYPE_EX_CARD_TIMEOUT,
|
||||
From: v.AUid,
|
||||
To: v.BUid,
|
||||
Type: MsgMod.HANDLE_TYPE_EX_CARD_TIMEOUT,
|
||||
SendT: GoUtil.Now(),
|
||||
End: GoUtil.Now() + tenyear,
|
||||
Extra: *v,
|
||||
})
|
||||
p.AddCard(v.CardId)
|
||||
CardMod.DelExCard(v)
|
||||
|
||||
@ -6,8 +6,9 @@ import (
|
||||
"server/game/mod/item"
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"strings"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -25,8 +25,9 @@ import (
|
||||
"server/game/mod/playroom"
|
||||
GoUtil "server/game_util"
|
||||
proto "server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
// 处理玩家异步请求
|
||||
@ -97,7 +98,7 @@ func (p *Player) handle(m *msg.Msg) error {
|
||||
case msg.HANDLE_TYPE_DEL: // 删除好友
|
||||
FriendMod := p.PlayMod.getFriendMod()
|
||||
FriendMod.DelFriend(m.From)
|
||||
p.AddLog(m.From, friend.LOG_TYPE_FRIEND_APPLY, "", m.SendT)
|
||||
//p.AddLog(m.From, friend.LOG_TYPE_FRIEND_DELETE, "", m.SendT)
|
||||
playerSimpleData := G_GameLogicPtr.GetResSimplePlayerByUid(m.From)
|
||||
p.PushClientRes(&proto.ResFriendApplyNotify{
|
||||
Player: playerSimpleData,
|
||||
@ -155,7 +156,11 @@ func (p *Player) handle(m *msg.Msg) error {
|
||||
p.PlayMod.save()
|
||||
p.PushClientRes(InviteMod.NotifySuccess())
|
||||
case msg.HANDLE_TYPE_SEND_CARD: // B收到A赠送的卡牌
|
||||
cardInfo := m.Extra.(card.CardInfo)
|
||||
cardInfo, ok := m.Extra.(card.CardInfo)
|
||||
if !ok {
|
||||
log.Error("Failed to cast m.Extra to card.CardInfo")
|
||||
return nil
|
||||
}
|
||||
FriendMod := p.PlayMod.getFriendMod()
|
||||
FriendMod.SetCardInfo(&cardInfo)
|
||||
FriendMod.Interact(cardInfo.AUid, friend.INTERACT_TYPE_CARD, m.SendT)
|
||||
@ -166,7 +171,11 @@ func (p *Player) handle(m *msg.Msg) error {
|
||||
)
|
||||
p.PlayMod.save()
|
||||
case msg.HANDLE_TYPE_EX_CARD_SELECT_TIMEOUT: // A收到B置换卡牌选择超时
|
||||
cardInfo := m.Extra.(card.CardInfo)
|
||||
cardInfo, ok := m.Extra.(card.CardInfo)
|
||||
if !ok {
|
||||
log.Error("Failed to cast m.Extra to card.CardInfo")
|
||||
return nil
|
||||
}
|
||||
CardMod := p.PlayMod.getCardMod()
|
||||
p.AddCard(cardInfo.CardId)
|
||||
CardMod.DelExCard(&cardInfo)
|
||||
@ -175,6 +184,7 @@ func (p *Player) handle(m *msg.Msg) error {
|
||||
case msg.HANDLE_TYPE_EX_CARD_TIMEOUT: // B收到A同意置换卡牌超时
|
||||
cardInfo, ok := m.Extra.(card.CardInfo)
|
||||
if !ok {
|
||||
log.Error("Failed to cast m.Extra to card.CardInfo")
|
||||
return nil
|
||||
}
|
||||
CardMod := p.PlayMod.getCardMod()
|
||||
@ -184,6 +194,7 @@ func (p *Player) handle(m *msg.Msg) error {
|
||||
case msg.HANDLE_TYPE_REG_CARD_FINISH, msg.HANDLE_TYPE_AGREE_CARD_FAIL: // B收到A的请求已结束
|
||||
cardInfo, ok := m.Extra.(card.CardInfo)
|
||||
if !ok {
|
||||
log.Error("Failed to cast m.Extra to card.CardInfo")
|
||||
return nil
|
||||
}
|
||||
FriendMod := p.PlayMod.getFriendMod()
|
||||
|
||||
@ -8,9 +8,10 @@ import (
|
||||
"server/conf"
|
||||
"server/game/mod/msg"
|
||||
GoUtil "server/game_util"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
var id = 1
|
||||
@ -89,6 +90,8 @@ func (m *MessageMgr) MessageMgrInit() {
|
||||
|
||||
// 注册处理函数
|
||||
m.init()
|
||||
// 启动时 清空玩家登录信息
|
||||
m.data.(*MessageData).PlayerList = make(map[int64]int)
|
||||
m.handler = make(map[int]MessageHandlerFunc)
|
||||
m.middlewares = []MessageMiddleware{}
|
||||
// 初始化 Worker Pool (10个worker, 1000个队列大小)
|
||||
|
||||
@ -15,14 +15,16 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
ACT_TYPE_MINING = 1 // 挖矿
|
||||
ACT_TYPE_GUESS_COLOR = 2 // 猜颜色
|
||||
ACT_TYPE_RACE = 3 // 赛跑
|
||||
ACT_TYPE_DISCOUNT_GIFT = 4 // 折扣礼包
|
||||
ACT_TYPE_ADD_GIFT = 5 // 买一赠一礼包
|
||||
ACT_TYPE_SUPER_GIFT = 6 // 超值加购礼包
|
||||
ACT_TYPE_CATNIP = 7 // 猫草大作战
|
||||
ACT_TYPE_PASS = 8 // 通行证
|
||||
ACT_TYPE_MINING = 1 // 挖矿
|
||||
ACT_TYPE_GUESS_COLOR = 2 // 猜颜色
|
||||
ACT_TYPE_RACE = 3 // 赛跑
|
||||
ACT_TYPE_DISCOUNT_GIFT = 4 // 折扣礼包
|
||||
ACT_TYPE_ADD_GIFT = 5 // 买一赠一礼包
|
||||
ACT_TYPE_SUPER_GIFT = 6 // 超值加购礼包
|
||||
ACT_TYPE_CATNIP = 7 // 猫草大作战
|
||||
ACT_TYPE_PASS = 8 // 通行证
|
||||
ACT_TYPE_CHAMPION = 9 // 冠军赛
|
||||
ACT_TYPE_CAT_RETURN_GIFT = 10 // 喵喵回礼
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -6,7 +6,8 @@ import (
|
||||
"server/game/mod/item"
|
||||
GoUtil "server/game_util"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
type CardMod struct {
|
||||
@ -45,6 +46,22 @@ const (
|
||||
HANDBOOK_STATUS_GET = 2 // 已领取
|
||||
)
|
||||
|
||||
/*
|
||||
请求卡牌
|
||||
AUid: 请求者
|
||||
BUid: 被请求者
|
||||
cardId: 请求的卡牌id
|
||||
赠送卡牌
|
||||
AUid: 赠送者
|
||||
BUid: 接收者
|
||||
CardId: 卡牌id
|
||||
卡牌交换
|
||||
AUid: 发起者
|
||||
BUid: 接收者
|
||||
CardId: 发起者提供的卡牌id
|
||||
ExId: 接收者提供的卡牌id
|
||||
*/
|
||||
|
||||
const (
|
||||
TYPE_CARD_GIVE = 1 // 请求卡牌
|
||||
TYPE_CARD_SEND = 2 // 赠送卡牌
|
||||
|
||||
48
src/server/game/mod/cat_return_gift/cat_return_gift.go
Normal file
48
src/server/game/mod/cat_return_gift/cat_return_gift.go
Normal file
@ -0,0 +1,48 @@
|
||||
package catreturngift
|
||||
|
||||
type CatReturnGiftMod struct {
|
||||
Id int
|
||||
AId int
|
||||
Score int
|
||||
Reward int
|
||||
}
|
||||
|
||||
func (c *CatReturnGiftMod) InitData() {}
|
||||
|
||||
func (c *CatReturnGiftMod) ZeroUpdate(id, aid int) (int, int, int) {
|
||||
return c.Login(id, aid)
|
||||
}
|
||||
|
||||
func (c *CatReturnGiftMod) Login(id, aid int) (int, int, int) {
|
||||
oldId := c.Id
|
||||
if aid == 0 {
|
||||
c.AId = 0
|
||||
return oldId, 0, 0
|
||||
}
|
||||
if c.AId == aid {
|
||||
return 0, 0, 0
|
||||
}
|
||||
score := c.Score
|
||||
reward := c.Reward
|
||||
c.AId = aid
|
||||
c.Id = id
|
||||
c.Score = 0
|
||||
c.Reward = 0
|
||||
return oldId, score, reward
|
||||
}
|
||||
|
||||
func (c *CatReturnGiftMod) GetReward() int {
|
||||
return c.Reward
|
||||
}
|
||||
|
||||
func (c *CatReturnGiftMod) GetScore() int {
|
||||
return c.Score
|
||||
}
|
||||
|
||||
func (c *CatReturnGiftMod) AddScore(score int) {
|
||||
c.Score += score
|
||||
}
|
||||
|
||||
func (c *CatReturnGiftMod) SetReward(reward int) {
|
||||
c.Reward = reward
|
||||
}
|
||||
@ -15,20 +15,19 @@ type ChampshipMod struct {
|
||||
RankReward bool
|
||||
PreMax int // 昨日最高档
|
||||
Max int // 历史最高档
|
||||
AId int // 活动id
|
||||
LastAId int // 上次活动id
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) InitData() {}
|
||||
|
||||
// isActive 判断当前是否在冠军赛活跃时段(零点后 5 分钟内为结算期,不计分)
|
||||
func (c *ChampshipMod) isActive() bool {
|
||||
return GoUtil.Now()-GoUtil.ZeroTimestamp() >= 300
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) ZeroUpdate() {
|
||||
func (c *ChampshipMod) ZeroUpdate(aid int) {
|
||||
c.PreMax = c.Reward
|
||||
c.Score = 0
|
||||
c.Reward = 0
|
||||
c.RankReward = false
|
||||
c.LastAId = c.AId
|
||||
c.AId = aid
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) GetScore() int {
|
||||
@ -39,14 +38,19 @@ func (c *ChampshipMod) GetRankReward(Rank, yesterdayActivityId int) ([]*item.Ite
|
||||
if c.RankReward {
|
||||
return nil, fmt.Errorf("rank reward has been received")
|
||||
}
|
||||
c.RankReward = true
|
||||
c.SetRankReward()
|
||||
return champshipCfg.GetRankReward(Rank, yesterdayActivityId), nil
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) SetRankReward() {
|
||||
c.RankReward = true
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) HasRankReward() bool {
|
||||
return c.RankReward
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) AddScore(chess []int) {
|
||||
if !c.isActive() {
|
||||
return
|
||||
}
|
||||
score := 0
|
||||
for _, v := range chess {
|
||||
Lv := mergeDataCfg.GetLvById(v)
|
||||
@ -63,6 +67,13 @@ func (c *ChampshipMod) GetReward(activityId, orderFactor int) []*item.Item {
|
||||
return items
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) GetRewardId() int {
|
||||
return c.Reward
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) SetRewardId(rewardId int) {
|
||||
c.Reward = rewardId
|
||||
}
|
||||
func (c *ChampshipMod) BackData(myRank, myPreRank, todayActivityId, yesterdayActivityId int) *msg.ResChampship {
|
||||
rankReward := 0
|
||||
if c.RankReward {
|
||||
@ -70,18 +81,12 @@ func (c *ChampshipMod) BackData(myRank, myPreRank, todayActivityId, yesterdayAct
|
||||
} else if myPreRank > 0 {
|
||||
rankReward = 1
|
||||
}
|
||||
status := 0
|
||||
if c.isActive() {
|
||||
status = 1
|
||||
}
|
||||
return &msg.ResChampship{
|
||||
Score: int32(c.Score),
|
||||
Reward: int32(c.Reward),
|
||||
EndTime: int32(GoUtil.ZeroTimestamp() + 86400),
|
||||
Period: int32(GoUtil.GetServerOpenDay()),
|
||||
Rank: int32(myRank),
|
||||
RankReward: int32(rankReward),
|
||||
Status: int32(status),
|
||||
TodayActivityId: int32(todayActivityId),
|
||||
YesterdayActivityId: int32(yesterdayActivityId),
|
||||
}
|
||||
@ -94,3 +99,7 @@ func (c *ChampshipMod) GetH() int {
|
||||
func (c *ChampshipMod) GetN() int {
|
||||
return c.Max
|
||||
}
|
||||
|
||||
func (c *ChampshipMod) GetAId() int {
|
||||
return c.AId
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@ import (
|
||||
"server/game/mod/order"
|
||||
GoUtil "server/game_util"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
type ChargeMod struct {
|
||||
|
||||
@ -7,8 +7,9 @@ import (
|
||||
"server/game/mod/quest"
|
||||
GoUtil "server/game_util"
|
||||
"server/msg"
|
||||
"server/pkg/github.com/name5566/leaf/log"
|
||||
"sort"
|
||||
|
||||
"gitea.bywaystudios.com/pet_home/leaf/log"
|
||||
)
|
||||
|
||||
type DailyTaskMod struct {
|
||||
|
||||
@ -98,7 +98,7 @@ func (d *Decorate) GetDecorateCostItem(areaId, decorateId int, decorateOffIsExis
|
||||
id := decorateCfg.GetIdBySenceAndLv(areaId, decorateId)
|
||||
if decorateOffIsExist {
|
||||
offRate := limitedTimeEventCfg.GetDecorateOffDiscount(areaId, decorateId)
|
||||
itemNum = int(math.Ceil(float64(itemNum) * float64(offRate)))
|
||||
itemNum = int(math.Round(float64(itemNum) * float64(offRate) / 100))
|
||||
}
|
||||
items := []*item.Item{item.NewItem(item.ITEM_STAR_ID, itemNum)}
|
||||
partCostInfo := d.PartCost[id]
|
||||
@ -157,7 +157,7 @@ func (d *Decorate) DecorateAll(star int, decorateOffIsExist bool) ([]*item.Item,
|
||||
needStar := decorateCfg.GetStarCost(d.AreaId, v)
|
||||
if decorateOffIsExist {
|
||||
offRate := limitedTimeEventCfg.GetDecorateOffDiscount(d.AreaId, v)
|
||||
needStar = int(math.Ceil(float64(needStar) * float64(offRate)))
|
||||
needStar = int(math.Round(float64(needStar) * float64(offRate) / 100))
|
||||
}
|
||||
if star < needStar {
|
||||
break
|
||||
|
||||
@ -61,6 +61,7 @@ type BubbleInfo struct {
|
||||
Time int64 // 气泡时间
|
||||
Type int
|
||||
ItemList []*item.Item // 奖励物品
|
||||
Uid int // 相关玩家uid
|
||||
}
|
||||
|
||||
type FriendInfo struct {
|
||||
@ -225,16 +226,6 @@ func (f *FriendMod) InitData(m_DwUin int64) {
|
||||
if f.NewApplyList == nil {
|
||||
f.NewApplyList = make(map[int]*ApplyInfo)
|
||||
}
|
||||
for k, v := range f.Card {
|
||||
if v.AUid != 0 && !f.CheckFriend(v.AUid) && v.AUid != int(m_DwUin) {
|
||||
delete(f.Card, k)
|
||||
continue
|
||||
}
|
||||
if v.BUid != 0 && !f.CheckFriend(v.BUid) && v.BUid != int(m_DwUin) {
|
||||
delete(f.Card, k)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.ReplyList) > 0 {
|
||||
validReplyList := make([]*ReplyInfo, 0, len(f.ReplyList))
|
||||
@ -273,6 +264,19 @@ func (f *FriendMod) version() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.Version == 1 {
|
||||
f.Version = 2
|
||||
for _, v := range f.Bubble {
|
||||
if v.Uid == 0 {
|
||||
for _, log := range f.Log {
|
||||
if log.Id == v.Id {
|
||||
v.Uid = log.Uid
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 零点更新
|
||||
@ -387,6 +391,12 @@ func (f *FriendMod) DelFriend(id int) {
|
||||
v.Status = 1
|
||||
}
|
||||
}
|
||||
// 删除好友后清除卡牌交换
|
||||
for k, v := range f.Card {
|
||||
if v.Type == card.TYPE_CARD_GIVE && id == v.AUid {
|
||||
delete(f.Card, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否好友
|
||||
@ -525,11 +535,11 @@ func (f *FriendMod) AddLog(uid, ltype int, param string, send int64) int {
|
||||
})
|
||||
switch ltype {
|
||||
case LOG_TYPE_HANDBOOK_UPVOTE, LOG_TYPE_PLAYROOM_UPVOTE:
|
||||
f.AddBubble(f.AutoId, ltype, nil)
|
||||
f.AddBubble(f.AutoId, uid, ltype, nil)
|
||||
case LOG_TYPE_TREASURE_HELP:
|
||||
itemNum := GoUtil.RandNum(2, 5)
|
||||
items := []*item.Item{item.NewItem(item.ITEM_STAR_ID, itemNum)}
|
||||
f.AddBubble(f.AutoId, ltype, items)
|
||||
f.AddBubble(f.AutoId, uid, ltype, items)
|
||||
}
|
||||
if len(f.Log) > 30 {
|
||||
f.Log = f.Log[len(f.Log)-30:]
|
||||
@ -538,9 +548,10 @@ func (f *FriendMod) AddLog(uid, ltype int, param string, send int64) int {
|
||||
}
|
||||
|
||||
// 增加气泡
|
||||
func (f *FriendMod) AddBubble(id, bType int, items []*item.Item) {
|
||||
func (f *FriendMod) AddBubble(id, uid, bType int, items []*item.Item) {
|
||||
f.Bubble[id] = &BubbleInfo{
|
||||
Id: id,
|
||||
Uid: uid,
|
||||
Time: GoUtil.Now(),
|
||||
Type: bType,
|
||||
ItemList: items,
|
||||
@ -554,6 +565,7 @@ func (f *FriendMod) GetBubble(id int) *msg.FriendBubbleInfo {
|
||||
Id: int32(v.Id),
|
||||
Type: int32(v.Type),
|
||||
Items: item.ItemToMsg(v.ItemList),
|
||||
Uid: int64(v.Uid),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -566,6 +578,7 @@ func (f *FriendMod) BubbleBackData() *msg.ResFriendBubble {
|
||||
Id: int32(v.Id),
|
||||
Type: int32(v.Type),
|
||||
Items: item.ItemToMsg(v.ItemList),
|
||||
Uid: int64(v.Uid),
|
||||
})
|
||||
}
|
||||
return &msg.ResFriendBubble{
|
||||
@ -760,3 +773,7 @@ func (f *FriendMod) CheckGreeting(uid int) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *FriendMod) GetCardList() map[string]*card.CardInfo {
|
||||
return f.Card
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user