From b6bc133589da5fcf60aabb7c2441575604b69a11 Mon Sep 17 00:00:00 2001 From: cbluebird Date: Fri, 17 Jan 2025 14:43:42 +0800 Subject: [PATCH 1/3] feat(circuit): add circuit breaker Signed-off-by: cbluebird --- .../zfController/zfController.go | 76 +++----------- app/controllers/userController/bind.go | 7 +- app/services/funnelServices/funnelServices.go | 4 +- app/services/funnelServices/libraryService.go | 4 +- app/services/funnelServices/zfService.go | 30 +++--- app/services/userServices/setUser.go | 9 +- app/utils/circuitBreaker/apiSnapShot.go | 51 ++++++++++ app/utils/circuitBreaker/circuitBreaker.go | 30 ++++++ app/utils/circuitBreaker/counter.go | 31 ++++++ app/utils/circuitBreaker/liveNessProbe.go | 59 +++++++++++ app/utils/circuitBreaker/loadbalance.go | 99 +++++++++++++++++++ config/LiveNess/config.go | 48 +++++++++ config/api/funnelApi/api.go | 9 ++ go.mod | 1 + go.sum | 2 + 15 files changed, 375 insertions(+), 85 deletions(-) create mode 100644 app/utils/circuitBreaker/apiSnapShot.go create mode 100644 app/utils/circuitBreaker/circuitBreaker.go create mode 100644 app/utils/circuitBreaker/counter.go create mode 100644 app/utils/circuitBreaker/liveNessProbe.go create mode 100644 app/utils/circuitBreaker/loadbalance.go create mode 100644 config/LiveNess/config.go create mode 100644 config/api/funnelApi/api.go diff --git a/app/controllers/funcControllers/zfController/zfController.go b/app/controllers/funcControllers/zfController/zfController.go index 2a4f2bd..0de51e4 100644 --- a/app/controllers/funcControllers/zfController/zfController.go +++ b/app/controllers/funcControllers/zfController/zfController.go @@ -4,14 +4,13 @@ import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" - "math/rand" "time" "wejh-go/app/apiException" - "wejh-go/app/models" "wejh-go/app/services/funnelServices" "wejh-go/app/services/sessionServices" "wejh-go/app/services/userServices" "wejh-go/app/utils" + "wejh-go/app/utils/circuitBreaker" "wejh-go/config/redis" ) @@ -34,15 +33,11 @@ func GetClassTable(c *gin.Context) { return } - loginType, err := genLoginType(user) - if err != nil { - _ = c.AbortWithError(200, err) - return - } + api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") - result, err := funnelServices.GetClassTable(user, postForm.Year, postForm.Term, loginType) + result, err := funnelServices.GetClassTable(user, postForm.Year, postForm.Term, api, loginType) if err != nil { - userServices.DelPassword(err, user, loginType) + userServices.DelPassword(err, user, string(loginType)) _ = c.AbortWithError(200, err) return } @@ -63,15 +58,11 @@ func GetScore(c *gin.Context) { return } - loginType, err := genLoginType(user) - if err != nil { - _ = c.AbortWithError(200, err) - return - } + api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") - result, err := funnelServices.GetScore(user, postForm.Year, postForm.Term, loginType) + result, err := funnelServices.GetScore(user, postForm.Year, postForm.Term, api, loginType) if err != nil { - userServices.DelPassword(err, user, loginType) + userServices.DelPassword(err, user, string(loginType)) _ = c.AbortWithError(200, err) return } @@ -92,15 +83,11 @@ func GetMidTermScore(c *gin.Context) { return } - loginType, err := genLoginType(user) - if err != nil { - _ = c.AbortWithError(200, err) - return - } + api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") - result, err := funnelServices.GetMidTermScore(user, postForm.Year, postForm.Term, loginType) + result, err := funnelServices.GetMidTermScore(user, postForm.Year, postForm.Term, api, loginType) if err != nil { - userServices.DelPassword(err, user, loginType) + userServices.DelPassword(err, user, string(loginType)) _ = c.AbortWithError(200, err) return } @@ -121,15 +108,11 @@ func GetExam(c *gin.Context) { return } - loginType, err := genLoginType(user) - if err != nil { - _ = c.AbortWithError(200, err) - return - } + api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") - result, err := funnelServices.GetExam(user, postForm.Year, postForm.Term, loginType) + result, err := funnelServices.GetExam(user, postForm.Year, postForm.Term, api, loginType) if err != nil { - userServices.DelPassword(err, user, loginType) + userServices.DelPassword(err, user, string(loginType)) _ = c.AbortWithError(200, err) return } @@ -159,11 +142,7 @@ func GetRoom(c *gin.Context) { return } - loginType, err := genLoginType(user) - if err != nil { - _ = c.AbortWithError(200, err) - return - } + api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") // 使用 Redis 缓存键,包含查询参数 cacheKey := fmt.Sprintf("room:%s:%s:%s:%s:%s:%s", postForm.Year, postForm.Term, postForm.Campus, postForm.Weekday, postForm.Week, postForm.Sections) @@ -181,9 +160,9 @@ func GetRoom(c *gin.Context) { } } - result, err := funnelServices.GetRoom(user, postForm.Year, postForm.Term, postForm.Campus, postForm.Weekday, postForm.Week, postForm.Sections, loginType) + result, err := funnelServices.GetRoom(user, postForm.Year, postForm.Term, postForm.Campus, postForm.Weekday, postForm.Week, postForm.Sections, api, loginType) if err != nil { - userServices.DelPassword(err, user, loginType) + userServices.DelPassword(err, user, string(loginType)) _ = c.AbortWithError(200, err) return } @@ -198,26 +177,3 @@ func GetRoom(c *gin.Context) { } utils.JsonSuccessResponse(c, result) } - -func genLoginType(u *models.User) (string, error) { - var loginType string - rand.Seed(time.Now().UnixNano()) - oauthVal := rand.Intn(40) - zfVal := rand.Intn(60) - - if u.OauthPassword != "" && u.ZFPassword != "" { - if oauthVal > zfVal { - loginType = "OAUTH" - } else { - loginType = "ZF" - } - } else if u.OauthPassword != "" { - loginType = "OAUTH" - } else if u.ZFPassword != "" { - loginType = "ZF" - } else { - return "", apiException.NoThatPasswordOrWrong - } - - return loginType, nil -} diff --git a/app/controllers/userController/bind.go b/app/controllers/userController/bind.go index bf41637..b062960 100644 --- a/app/controllers/userController/bind.go +++ b/app/controllers/userController/bind.go @@ -6,6 +6,7 @@ import ( "wejh-go/app/services/userServices" "wejh-go/app/services/yxyServices" "wejh-go/app/utils" + "wejh-go/app/utils/circuitBreaker" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -36,7 +37,8 @@ func BindZFPassword(c *gin.Context) { _ = c.AbortWithError(200, apiException.NotLogin) return } - err = userServices.SetZFPassword(user, postForm.PassWord) + api, _ := circuitBreaker.CB.GetApi(true, false) + err = userServices.SetZFPassword(user, postForm.PassWord, api) if err != nil { _ = c.AbortWithError(200, err) return @@ -56,7 +58,8 @@ func BindOauthPassword(c *gin.Context) { _ = c.AbortWithError(200, apiException.NotLogin) return } - err = userServices.SetOauthPassword(user, postForm.PassWord) + api, _ := circuitBreaker.CB.GetApi(false, true) + err = userServices.SetOauthPassword(user, postForm.PassWord, api) if err != nil { _ = c.AbortWithError(200, err) return diff --git a/app/services/funnelServices/funnelServices.go b/app/services/funnelServices/funnelServices.go index 368d071..5591ce4 100644 --- a/app/services/funnelServices/funnelServices.go +++ b/app/services/funnelServices/funnelServices.go @@ -14,10 +14,10 @@ type FunnelResponse struct { Data interface{} `json:"data"` } -func FetchHandleOfPost(form url.Values, url funnelApi.FunnelApi) (interface{}, error) { +func FetchHandleOfPost(form url.Values, host string, url funnelApi.FunnelApi) (interface{}, error) { f := fetch.Fetch{} f.Init() - res, err := f.PostForm(funnelApi.FunnelHost+string(url), form) + res, err := f.PostForm(host+string(url), form) if err != nil { return nil, apiException.RequestError } diff --git a/app/services/funnelServices/libraryService.go b/app/services/funnelServices/libraryService.go index 4fc6203..ac6a93d 100644 --- a/app/services/funnelServices/libraryService.go +++ b/app/services/funnelServices/libraryService.go @@ -14,7 +14,7 @@ func GetCurrentBorrow(u *models.User) (interface{}, error) { form := url.Values{} form.Add("username", u.StudentID) form.Add("password", u.OauthPassword) - return FetchHandleOfPost(form, funnelApi.LibraryCurrent) + return FetchHandleOfPost(form, funnelApi.FunnelHost, funnelApi.LibraryCurrent) } func GetHistoryBorrow(u *models.User) (interface{}, error) { @@ -25,5 +25,5 @@ func GetHistoryBorrow(u *models.User) (interface{}, error) { form.Add("username", u.StudentID) form.Add("password", u.OauthPassword) - return FetchHandleOfPost(form, funnelApi.LibraryHistory) + return FetchHandleOfPost(form, funnelApi.FunnelHost, funnelApi.LibraryHistory) } diff --git a/app/services/funnelServices/zfService.go b/app/services/funnelServices/zfService.go index ea9e030..e9238c1 100644 --- a/app/services/funnelServices/zfService.go +++ b/app/services/funnelServices/zfService.go @@ -6,7 +6,7 @@ import ( "wejh-go/config/api/funnelApi" ) -func genTermForm(u *models.User, year, term, loginType string) url.Values { +func genTermForm(u *models.User, year, term string, loginType funnelApi.LoginType) url.Values { var password string if loginType == "OAUTH" { @@ -18,42 +18,42 @@ func genTermForm(u *models.User, year, term, loginType string) url.Values { form := url.Values{} form.Add("username", u.StudentID) form.Add("password", password) - form.Add("type", loginType) + form.Add("type", string(loginType)) form.Add("year", year) form.Add("term", term) return form } -func GetClassTable(u *models.User, year, term, loginType string) (interface{}, error) { +func GetClassTable(u *models.User, year, term, host string, loginType funnelApi.LoginType) (interface{}, error) { form := genTermForm(u, year, term, loginType) - return FetchHandleOfPost(form, funnelApi.ZFClassTable) + return FetchHandleOfPost(form, host, funnelApi.ZFClassTable) } -func GetScore(u *models.User, year, term, loginType string) (interface{}, error) { +func GetScore(u *models.User, year, term, host string, loginType funnelApi.LoginType) (interface{}, error) { form := genTermForm(u, year, term, loginType) - return FetchHandleOfPost(form, funnelApi.ZFScore) + return FetchHandleOfPost(form, host, funnelApi.ZFScore) } -func GetMidTermScore(u *models.User, year, term, loginType string) (interface{}, error) { +func GetMidTermScore(u *models.User, year, term, host string, loginType funnelApi.LoginType) (interface{}, error) { form := genTermForm(u, year, term, loginType) - return FetchHandleOfPost(form, funnelApi.ZFMidTermScore) + return FetchHandleOfPost(form, host, funnelApi.ZFMidTermScore) } -func GetExam(u *models.User, year, term, loginType string) (interface{}, error) { +func GetExam(u *models.User, year, term, host string, loginType funnelApi.LoginType) (interface{}, error) { form := genTermForm(u, year, term, loginType) - return FetchHandleOfPost(form, funnelApi.ZFExam) + return FetchHandleOfPost(form, host, funnelApi.ZFExam) } -func GetRoom(u *models.User, year, term, campus, weekday, week, sections, loginType string) (interface{}, error) { +func GetRoom(u *models.User, year, term, campus, weekday, week, sections, host string, loginType funnelApi.LoginType) (interface{}, error) { form := genTermForm(u, year, term, loginType) form.Add("campus", campus) form.Add("weekday", weekday) form.Add("week", week) form.Add("sections", sections) - return FetchHandleOfPost(form, funnelApi.ZFRoom) + return FetchHandleOfPost(form, host, funnelApi.ZFRoom) } -func BindPassword(u *models.User, year, term, loginType string) (interface{}, error) { +func BindPassword(u *models.User, year, term, host string, loginType funnelApi.LoginType) (interface{}, error) { var password string if loginType == "ZF" { password = u.ZFPassword @@ -63,8 +63,8 @@ func BindPassword(u *models.User, year, term, loginType string) (interface{}, er form := url.Values{} form.Add("username", u.StudentID) form.Add("password", password) - form.Add("type", loginType) + form.Add("type", string(loginType)) form.Add("year", year) form.Add("term", term) - return FetchHandleOfPost(form, funnelApi.ZFExam) + return FetchHandleOfPost(form, host, funnelApi.ZFExam) } diff --git a/app/services/userServices/setUser.go b/app/services/userServices/setUser.go index 4d2b2b1..aa564cd 100644 --- a/app/services/userServices/setUser.go +++ b/app/services/userServices/setUser.go @@ -5,12 +5,13 @@ import ( "wejh-go/app/apiException" "wejh-go/app/models" "wejh-go/app/services/funnelServices" + "wejh-go/config/api/funnelApi" "wejh-go/config/database" ) -func SetZFPassword(user *models.User, password string) error { +func SetZFPassword(user *models.User, password, api string) error { user.ZFPassword = password - _, err := funnelServices.BindPassword(user, string(rune(time.Now().Year())), "3", "ZF") + _, err := funnelServices.BindPassword(user, string(rune(time.Now().Year())), "3", api, funnelApi.ZF) if err != nil { return err } @@ -19,9 +20,9 @@ func SetZFPassword(user *models.User, password string) error { return nil } -func SetOauthPassword(user *models.User, password string) error { +func SetOauthPassword(user *models.User, password, api string) error { user.OauthPassword = password - _, err := funnelServices.BindPassword(user, string(rune(time.Now().Year())), "3", "OAUTH") + _, err := funnelServices.BindPassword(user, string(rune(time.Now().Year())), "3", api, funnelApi.Oauth) if err != nil { return err } diff --git a/app/utils/circuitBreaker/apiSnapShot.go b/app/utils/circuitBreaker/apiSnapShot.go new file mode 100644 index 0000000..e626f1f --- /dev/null +++ b/app/utils/circuitBreaker/apiSnapShot.go @@ -0,0 +1,51 @@ +package circuitBreaker + +import ( + "time" + "wejh-go/config/api/funnelApi" +) + +type apiSnapShot struct { + LoginType funnelApi.LoginType + State State + ErrCount Counter + TotalCount Counter + AccessLast time.Time +} + +func (a *apiSnapShot) Fail() bool { + a.ErrCount.Add(1) + a.TotalCount.Add(1) + a.AccessLast = time.Now() + if a.ErrCount.Get() > 10 { + a.State = Open + return true + } + return false +} + +func (a *apiSnapShot) Success() { + a.ErrCount.Zero() + a.State = Closed + a.AccessLast = time.Now() +} + +type State int32 + +func (s State) String() string { + switch s { + case Open: + return "OPEN" + case HalfOpen: + return "HALFOPEN" + case Closed: + return "CLOSED" + } + return "INVALID" +} + +const ( + Open State = iota + HalfOpen + Closed +) diff --git a/app/utils/circuitBreaker/circuitBreaker.go b/app/utils/circuitBreaker/circuitBreaker.go new file mode 100644 index 0000000..74cf246 --- /dev/null +++ b/app/utils/circuitBreaker/circuitBreaker.go @@ -0,0 +1,30 @@ +package circuitBreaker + +import "wejh-go/config/api/funnelApi" + +var CB CircuitBreaker + +func init() { + //Todo: add config +} + +type CircuitBreaker struct { + lb LoadBalance + SnapShot map[string]*apiSnapShot +} + +func (c *CircuitBreaker) GetApi(zfFlag, oauthFlag bool) (string, funnelApi.LoginType) { + return c.lb.Pick(zfFlag, oauthFlag) +} + +func (c *CircuitBreaker) Fail(api string) { + if c.SnapShot[api].Fail() { + c.lb.Remove(api, c.SnapShot[api].LoginType) + Probe.Add(api, c.SnapShot[api].LoginType) + } +} + +func (c *CircuitBreaker) Success(api string) { + c.lb.Add(api, c.SnapShot[api].LoginType) + c.SnapShot[api].Success() +} diff --git a/app/utils/circuitBreaker/counter.go b/app/utils/circuitBreaker/counter.go new file mode 100644 index 0000000..f37b0ac --- /dev/null +++ b/app/utils/circuitBreaker/counter.go @@ -0,0 +1,31 @@ +package circuitBreaker + +import ( + "sync/atomic" +) + +type Counter interface { + Add(i int64) + Get() int64 + Zero() +} + +type atomicCounter struct { + x int64 +} + +func (c *atomicCounter) Add(i int64) { + atomic.AddInt64(&c.x, i) +} + +func (c *atomicCounter) Get() int64 { + return atomic.LoadInt64(&c.x) +} + +func (c *atomicCounter) Zero() { + atomic.StoreInt64(&c.x, 0) +} + +const ( + cacheLineSize = 64 +) diff --git a/app/utils/circuitBreaker/liveNessProbe.go b/app/utils/circuitBreaker/liveNessProbe.go new file mode 100644 index 0000000..96df8a6 --- /dev/null +++ b/app/utils/circuitBreaker/liveNessProbe.go @@ -0,0 +1,59 @@ +package circuitBreaker + +import ( + "sync" + "time" + "wejh-go/app/models" + "wejh-go/app/services/funnelServices" + liveNessConfig "wejh-go/config/LiveNess" + "wejh-go/config/api/funnelApi" +) + +var Probe LiveNessProbe +var user *models.User + +func init() { + Probe = LiveNessProbe{ + Config: liveNessConfig.GetLiveNessConfig(), + ApiMap: make(map[string]funnelApi.LoginType), + } + user.OauthPassword = Probe.Config.OauthPassword + user.ZFPassword = Probe.Config.ZFPassword + user.StudentID = Probe.Config.StudentId + go Probe.Start() +} + +type LiveNessProbe struct { + sync.Mutex + Config liveNessConfig.LiveNessProbeConfig + ApiMap map[string]funnelApi.LoginType +} + +func (l *LiveNessProbe) Add(api string, loginType funnelApi.LoginType) { + l.Lock() + defer l.Unlock() + l.ApiMap[api] = loginType +} + +func (l *LiveNessProbe) Remove(key string) { + l.Lock() + defer l.Unlock() + delete(l.ApiMap, key) +} + +func (l *LiveNessProbe) Start() { + ticker := time.NewTicker(l.Config.Duration) + defer ticker.Stop() + for { + select { + case <-ticker.C: + for api, loginType := range l.ApiMap { + _, err := funnelServices.GetClassTable(user, "2023", "上", api, loginType) + if err == nil { + CB.Success(api) + delete(l.ApiMap, api) + } + } + } + } +} diff --git a/app/utils/circuitBreaker/loadbalance.go b/app/utils/circuitBreaker/loadbalance.go new file mode 100644 index 0000000..040b07d --- /dev/null +++ b/app/utils/circuitBreaker/loadbalance.go @@ -0,0 +1,99 @@ +package circuitBreaker + +import ( + "github.com/bytedance/gopkg/lang/fastrand" + "wejh-go/config/api/funnelApi" +) + +type LoadBalanceType int + +const ( + RoundRobin LoadBalanceType = iota + Random +) + +type LoadBalance struct { + zfLB *randomLB + oauthLB *randomLB +} + +func (lb *LoadBalance) Pick(zfFlag, oauthFlag bool) (string, funnelApi.LoginType) { + var loginType funnelApi.LoginType + + if oauthFlag && zfFlag { + if fastrand.Intn(100) > 50 { + loginType = funnelApi.Oauth + } else { + loginType = funnelApi.ZF + } + } else if oauthFlag { + loginType = funnelApi.Oauth + } else if zfFlag { + loginType = funnelApi.ZF + } else { + return "", funnelApi.Unknown + } + if loginType == funnelApi.Oauth { + return lb.zfLB.Pick(), loginType + } + return lb.oauthLB.Pick(), loginType +} + +func (lb *LoadBalance) Remove(api string, loginType funnelApi.LoginType) { + if loginType == funnelApi.Oauth { + lb.oauthLB.Remove(api) + } else { + lb.zfLB.Remove(api) + } +} + +func (lb *LoadBalance) Add(api string, loginType funnelApi.LoginType) { + if loginType == funnelApi.Oauth { + lb.oauthLB.Add(api) + } else { + lb.zfLB.Add(api) + } +} + +type loadBalance interface { + LoadBalance() LoadBalanceType + Pick() (api string) + ReBalance(apis []string) + Remove(api string) +} + +type randomLB struct { + Api []string + Size int +} + +func newRandomLB(apis []string) loadBalance { + return &randomLB{Api: apis, Size: len(apis)} +} + +func (b *randomLB) LoadBalance() LoadBalanceType { + return Random +} + +func (b *randomLB) Pick() string { + idx := fastrand.Intn(b.Size) + return b.Api[idx] +} + +func (b *randomLB) ReBalance(apis []string) { + b.Api, b.Size = apis, len(apis) +} + +func (b *randomLB) Add(api ...string) { + b.Api = append(b.Api, api...) + b.Size = len(b.Api) +} + +func (b *randomLB) Remove(api string) { + for i, s := range b.Api { + if s == api { + b.Api = append(b.Api[:i], b.Api[i+1:]...) + break + } + } +} diff --git a/config/LiveNess/config.go b/config/LiveNess/config.go new file mode 100644 index 0000000..70260be --- /dev/null +++ b/config/LiveNess/config.go @@ -0,0 +1,48 @@ +package liveNessConfig + +import ( + "github.com/spf13/viper" + "log" + "time" + "wejh-go/config/api/funnelApi" + "wejh-go/config/config" +) + +type LiveNessProbeConfig struct { + StudentId string + OauthPassword string + ZFPassword string + Duration time.Duration +} + +func GetLiveNessConfig() LiveNessProbeConfig { + cfg := LiveNessProbeConfig{} + if config.Config.IsSet("liveNess.studentId") { + cfg.StudentId = config.Config.GetString("liveNess.studentId") + } + if config.Config.IsSet("liveNess.oauthPassword") { + cfg.OauthPassword = config.Config.GetString("liveNess.oauthPassword") + } + if config.Config.IsSet("liveNess.zfPassword") { + cfg.ZFPassword = config.Config.GetString("liveNess.zfPassword") + } + if config.Config.IsSet("liveNess.duration") { + cfg.Duration = config.Config.GetDuration("liveNess.duration") + } + return cfg +} + +type LoadBalanceConfig struct { + Name string + Url string + Type funnelApi.LoginType +} + +func GetLoadBalanceConfig() []LoadBalanceConfig { + var configs []LoadBalanceConfig + err := viper.UnmarshalKey("loadBalance", &configs) + if err != nil { + log.Fatalf("unable to decode into struct, %v", err) + } + return configs +} diff --git a/config/api/funnelApi/api.go b/config/api/funnelApi/api.go new file mode 100644 index 0000000..2d84007 --- /dev/null +++ b/config/api/funnelApi/api.go @@ -0,0 +1,9 @@ +package funnelApi + +type LoginType string + +const ( + Oauth LoginType = "OAUTH" + ZF = "ZF" + Unknown = "Unknown" +) diff --git a/go.mod b/go.mod index e5a8ce0..063fae7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module wejh-go go 1.22.9 require ( + github.com/bytedance/gopkg v0.1.1 github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/sessions v1.0.1 github.com/gin-gonic/gin v1.10.0 diff --git a/go.sum b/go.sum index f47526d..46f640f 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE= +github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= From b9e317e750c7a45e918d9d7d89285261bcad2682 Mon Sep 17 00:00:00 2001 From: cbluebird Date: Fri, 24 Jan 2025 19:06:38 +0800 Subject: [PATCH 2/3] feat(circuit): add circuit breaker init config Signed-off-by: cbluebird --- app/utils/circuitBreaker/circuitBreaker.go | 21 +++++++++++++++++++-- config/LiveNess/config.go | 6 +++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/utils/circuitBreaker/circuitBreaker.go b/app/utils/circuitBreaker/circuitBreaker.go index 74cf246..9f3adec 100644 --- a/app/utils/circuitBreaker/circuitBreaker.go +++ b/app/utils/circuitBreaker/circuitBreaker.go @@ -1,11 +1,28 @@ package circuitBreaker -import "wejh-go/config/api/funnelApi" +import ( + liveNessConfig "wejh-go/config/LiveNess" + "wejh-go/config/api/funnelApi" +) var CB CircuitBreaker func init() { - //Todo: add config + lb := LoadBalance{ + zfLB: &randomLB{}, + oauthLB: &randomLB{}, + } + for _, config := range liveNessConfig.GetLoadBalanceConfig() { + if config.Type == funnelApi.Oauth { + lb.oauthLB.Add(config.Url) + } else if config.Type == funnelApi.ZF { + lb.zfLB.Add(config.Url) + } + } + CB = CircuitBreaker{ + lb: lb, + SnapShot: make(map[string]*apiSnapShot), + } } type CircuitBreaker struct { diff --git a/config/LiveNess/config.go b/config/LiveNess/config.go index 70260be..ef2e3f4 100644 --- a/config/LiveNess/config.go +++ b/config/LiveNess/config.go @@ -33,9 +33,9 @@ func GetLiveNessConfig() LiveNessProbeConfig { } type LoadBalanceConfig struct { - Name string - Url string - Type funnelApi.LoginType + Name string `json:"name"` + Url string `json:"url"` + Type funnelApi.LoginType `json:"type"` } func GetLoadBalanceConfig() []LoadBalanceConfig { From 426b68598df0baa1800f85ff97722f03f4a13b5c Mon Sep 17 00:00:00 2001 From: XiMo-210 <2831802697@qq.com> Date: Wed, 29 Jan 2025 16:21:51 +0800 Subject: [PATCH 3/3] feat(circuit): add circuit breaker relevant logic close #72 --- app/apiException/apiException.go | 1 + .../zfController/zfController.go | 30 +++++- app/controllers/userController/bind.go | 12 ++- app/services/funnelServices/funnelServices.go | 81 +++++++-------- app/services/funnelServices/zfService.go | 2 +- app/utils/circuitBreaker/apiSnapShot.go | 22 +++-- app/utils/circuitBreaker/circuitBreaker.go | 47 ++++----- app/utils/circuitBreaker/counter.go | 4 - app/utils/circuitBreaker/liveNessProbe.go | 98 ++++++++++++++----- app/utils/circuitBreaker/loadbalance.go | 55 ++++++----- config.example.yaml | 10 +- config/LiveNess/config.go | 48 --------- config/api/funnelApi/api.go | 4 +- config/circuitBreaker/config.go | 32 ++++++ main.go | 43 +++++++- 15 files changed, 308 insertions(+), 181 deletions(-) delete mode 100644 config/LiveNess/config.go create mode 100644 config/circuitBreaker/config.go diff --git a/app/apiException/apiException.go b/app/apiException/apiException.go index 9e6b6fc..60a00d4 100644 --- a/app/apiException/apiException.go +++ b/app/apiException/apiException.go @@ -36,6 +36,7 @@ var ( SendVerificationCodeLimit = NewError(http.StatusInternalServerError, 200524, "短信发送超限,请1分钟后再试") CampusMismatch = NewError(http.StatusInternalServerError, 200525, "暂无该校区绑定信息") OAuthNotUpdate = NewError(http.StatusInternalServerError, 200526, "统一身份认证密码未更新") + NoApiAvailable = NewError(http.StatusInternalServerError, 200527, "正方相关服务暂不可用") NotInit = NewError(http.StatusNotFound, 200404, http.StatusText(http.StatusNotFound)) NotFound = NewError(http.StatusNotFound, 200404, http.StatusText(http.StatusNotFound)) Unknown = NewError(http.StatusInternalServerError, 300500, "系统异常,请稍后重试!") diff --git a/app/controllers/funcControllers/zfController/zfController.go b/app/controllers/funcControllers/zfController/zfController.go index 0de51e4..af78b83 100644 --- a/app/controllers/funcControllers/zfController/zfController.go +++ b/app/controllers/funcControllers/zfController/zfController.go @@ -33,7 +33,11 @@ func GetClassTable(c *gin.Context) { return } - api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + api, loginType, err := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + if err != nil { + _ = c.AbortWithError(200, err) + return + } result, err := funnelServices.GetClassTable(user, postForm.Year, postForm.Term, api, loginType) if err != nil { @@ -58,7 +62,11 @@ func GetScore(c *gin.Context) { return } - api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + api, loginType, err := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + if err != nil { + _ = c.AbortWithError(200, err) + return + } result, err := funnelServices.GetScore(user, postForm.Year, postForm.Term, api, loginType) if err != nil { @@ -83,7 +91,11 @@ func GetMidTermScore(c *gin.Context) { return } - api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + api, loginType, err := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + if err != nil { + _ = c.AbortWithError(200, err) + return + } result, err := funnelServices.GetMidTermScore(user, postForm.Year, postForm.Term, api, loginType) if err != nil { @@ -108,7 +120,11 @@ func GetExam(c *gin.Context) { return } - api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + api, loginType, err := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + if err != nil { + _ = c.AbortWithError(200, err) + return + } result, err := funnelServices.GetExam(user, postForm.Year, postForm.Term, api, loginType) if err != nil { @@ -142,7 +158,11 @@ func GetRoom(c *gin.Context) { return } - api, loginType := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + api, loginType, err := circuitBreaker.CB.GetApi(user.ZFPassword != "", user.OauthPassword != "") + if err != nil { + _ = c.AbortWithError(200, err) + return + } // 使用 Redis 缓存键,包含查询参数 cacheKey := fmt.Sprintf("room:%s:%s:%s:%s:%s:%s", postForm.Year, postForm.Term, postForm.Campus, postForm.Weekday, postForm.Week, postForm.Sections) diff --git a/app/controllers/userController/bind.go b/app/controllers/userController/bind.go index b062960..9f17108 100644 --- a/app/controllers/userController/bind.go +++ b/app/controllers/userController/bind.go @@ -37,7 +37,11 @@ func BindZFPassword(c *gin.Context) { _ = c.AbortWithError(200, apiException.NotLogin) return } - api, _ := circuitBreaker.CB.GetApi(true, false) + api, _, err := circuitBreaker.CB.GetApi(true, false) + if err != nil { + _ = c.AbortWithError(200, err) + return + } err = userServices.SetZFPassword(user, postForm.PassWord, api) if err != nil { _ = c.AbortWithError(200, err) @@ -58,7 +62,11 @@ func BindOauthPassword(c *gin.Context) { _ = c.AbortWithError(200, apiException.NotLogin) return } - api, _ := circuitBreaker.CB.GetApi(false, true) + api, _, err := circuitBreaker.CB.GetApi(false, true) + if err != nil { + _ = c.AbortWithError(200, err) + return + } err = userServices.SetOauthPassword(user, postForm.PassWord, api) if err != nil { _ = c.AbortWithError(200, err) diff --git a/app/services/funnelServices/funnelServices.go b/app/services/funnelServices/funnelServices.go index 5591ce4..ae176c7 100644 --- a/app/services/funnelServices/funnelServices.go +++ b/app/services/funnelServices/funnelServices.go @@ -3,7 +3,9 @@ package funnelServices import ( "encoding/json" "net/url" + "strings" "wejh-go/app/apiException" + "wejh-go/app/utils/circuitBreaker" "wejh-go/app/utils/fetch" "wejh-go/config/api/funnelApi" ) @@ -17,51 +19,52 @@ type FunnelResponse struct { func FetchHandleOfPost(form url.Values, host string, url funnelApi.FunnelApi) (interface{}, error) { f := fetch.Fetch{} f.Init() - res, err := f.PostForm(host+string(url), form) - if err != nil { - return nil, apiException.RequestError - } - rc := FunnelResponse{} - err = json.Unmarshal(res, &rc) - if err != nil { - return nil, apiException.RequestError - } - i := 0 - for rc.Code == 413 && i < 5 { - i++ - res, err = f.PostForm(funnelApi.FunnelHost+string(url), form) + + var rc FunnelResponse + var res []byte + var err error + for i := 0; i < 5; i++ { + res, err = f.PostForm(host+string(url), form) if err != nil { - return nil, apiException.RequestError + err = apiException.RequestError + break } - rc = FunnelResponse{} - err = json.Unmarshal(res, &rc) - if err != nil { - return nil, apiException.RequestError + if err = json.Unmarshal(res, &rc); err != nil { + err = apiException.RequestError + break + } + if rc.Code != 413 { + break } } - if rc.Code == 413 { - return rc.Data, apiException.ServerError - } - if rc.Code == 412 { - return rc.Data, apiException.NoThatPasswordOrWrong - } - if rc.Code == 416 { - return rc.Data, apiException.OAuthNotUpdate - } - return rc.Data, nil -} -func FetchHandleOfGet(url funnelApi.FunnelApi) (interface{}, error) { - f := fetch.Fetch{} - f.Init() - res, err := f.Get(funnelApi.FunnelHost + string(url)) + loginType := funnelApi.LoginType(form.Get("type")) + zfFlag := strings.Contains(string(url), "zf") if err != nil { - return nil, apiException.RequestError + if zfFlag { + circuitBreaker.CB.Fail(host, loginType) + } + return nil, apiException.ServerError } - rc := FunnelResponse{} - err = json.Unmarshal(res, &rc) - if err != nil { - return nil, apiException.RequestError + + if zfFlag { + if rc.Code == 200 || rc.Code == 412 || rc.Code == 416 { + circuitBreaker.CB.Success(host, loginType) + } else { + circuitBreaker.CB.Fail(host, loginType) + } + } + + switch rc.Code { + case 200: + return rc.Data, nil + case 413: + return nil, apiException.ServerError + case 412: + return nil, apiException.NoThatPasswordOrWrong + case 416: + return nil, apiException.OAuthNotUpdate + default: + return nil, apiException.ServerError } - return rc.Data, nil } diff --git a/app/services/funnelServices/zfService.go b/app/services/funnelServices/zfService.go index e9238c1..aecd9d4 100644 --- a/app/services/funnelServices/zfService.go +++ b/app/services/funnelServices/zfService.go @@ -9,7 +9,7 @@ import ( func genTermForm(u *models.User, year, term string, loginType funnelApi.LoginType) url.Values { var password string - if loginType == "OAUTH" { + if loginType == funnelApi.Oauth { password = u.OauthPassword } else { password = u.ZFPassword diff --git a/app/utils/circuitBreaker/apiSnapShot.go b/app/utils/circuitBreaker/apiSnapShot.go index e626f1f..02bb3bb 100644 --- a/app/utils/circuitBreaker/apiSnapShot.go +++ b/app/utils/circuitBreaker/apiSnapShot.go @@ -2,29 +2,35 @@ package circuitBreaker import ( "time" - "wejh-go/config/api/funnelApi" ) -type apiSnapShot struct { - LoginType funnelApi.LoginType +type ApiSnapShot struct { State State ErrCount Counter TotalCount Counter AccessLast time.Time } -func (a *apiSnapShot) Fail() bool { +func NewApiSnapShot() *ApiSnapShot { + return &ApiSnapShot{ + State: Closed, + ErrCount: &atomicCounter{}, + TotalCount: &atomicCounter{}, + AccessLast: time.Time{}, + } +} + +func (a *ApiSnapShot) Fail() bool { a.ErrCount.Add(1) a.TotalCount.Add(1) - a.AccessLast = time.Now() - if a.ErrCount.Get() > 10 { + if a.ErrCount.Get() > 50 { a.State = Open return true } return false } -func (a *apiSnapShot) Success() { +func (a *ApiSnapShot) Success() { a.ErrCount.Zero() a.State = Closed a.AccessLast = time.Now() @@ -37,7 +43,7 @@ func (s State) String() string { case Open: return "OPEN" case HalfOpen: - return "HALFOPEN" + return "HALF_OPEN" case Closed: return "CLOSED" } diff --git a/app/utils/circuitBreaker/circuitBreaker.go b/app/utils/circuitBreaker/circuitBreaker.go index 9f3adec..4ff9524 100644 --- a/app/utils/circuitBreaker/circuitBreaker.go +++ b/app/utils/circuitBreaker/circuitBreaker.go @@ -1,47 +1,48 @@ package circuitBreaker import ( - liveNessConfig "wejh-go/config/LiveNess" "wejh-go/config/api/funnelApi" + cbConfig "wejh-go/config/circuitBreaker" ) var CB CircuitBreaker +type CircuitBreaker struct { + LB LoadBalance + SnapShot map[string]*ApiSnapShot +} + func init() { lb := LoadBalance{ zfLB: &randomLB{}, oauthLB: &randomLB{}, } - for _, config := range liveNessConfig.GetLoadBalanceConfig() { - if config.Type == funnelApi.Oauth { - lb.oauthLB.Add(config.Url) - } else if config.Type == funnelApi.ZF { - lb.zfLB.Add(config.Url) - } + snapShot := make(map[string]*ApiSnapShot) + + for _, api := range cbConfig.GetLoadBalanceConfig().Apis { + lb.Add(api, funnelApi.Oauth) + lb.Add(api, funnelApi.ZF) + snapShot[api+string(funnelApi.Oauth)] = NewApiSnapShot() + snapShot[api+string(funnelApi.ZF)] = NewApiSnapShot() } + CB = CircuitBreaker{ - lb: lb, - SnapShot: make(map[string]*apiSnapShot), + LB: lb, + SnapShot: snapShot, } } -type CircuitBreaker struct { - lb LoadBalance - SnapShot map[string]*apiSnapShot -} - -func (c *CircuitBreaker) GetApi(zfFlag, oauthFlag bool) (string, funnelApi.LoginType) { - return c.lb.Pick(zfFlag, oauthFlag) +func (c *CircuitBreaker) GetApi(zfFlag, oauthFlag bool) (string, funnelApi.LoginType, error) { + return c.LB.Pick(zfFlag, oauthFlag) } -func (c *CircuitBreaker) Fail(api string) { - if c.SnapShot[api].Fail() { - c.lb.Remove(api, c.SnapShot[api].LoginType) - Probe.Add(api, c.SnapShot[api].LoginType) +func (c *CircuitBreaker) Fail(api string, loginType funnelApi.LoginType) { + if c.SnapShot[api+string(loginType)].Fail() { + c.LB.Remove(api, loginType) + Probe.Add(api, loginType) } } -func (c *CircuitBreaker) Success(api string) { - c.lb.Add(api, c.SnapShot[api].LoginType) - c.SnapShot[api].Success() +func (c *CircuitBreaker) Success(api string, loginType funnelApi.LoginType) { + c.SnapShot[api+string(loginType)].Success() } diff --git a/app/utils/circuitBreaker/counter.go b/app/utils/circuitBreaker/counter.go index f37b0ac..2d0e809 100644 --- a/app/utils/circuitBreaker/counter.go +++ b/app/utils/circuitBreaker/counter.go @@ -25,7 +25,3 @@ func (c *atomicCounter) Get() int64 { func (c *atomicCounter) Zero() { atomic.StoreInt64(&c.x, 0) } - -const ( - cacheLineSize = 64 -) diff --git a/app/utils/circuitBreaker/liveNessProbe.go b/app/utils/circuitBreaker/liveNessProbe.go index 96df8a6..4e5ab82 100644 --- a/app/utils/circuitBreaker/liveNessProbe.go +++ b/app/utils/circuitBreaker/liveNessProbe.go @@ -1,38 +1,51 @@ package circuitBreaker import ( + "context" + "encoding/json" + "net/url" + "strconv" + "strings" "sync" "time" + "wejh-go/app/apiException" "wejh-go/app/models" - "wejh-go/app/services/funnelServices" - liveNessConfig "wejh-go/config/LiveNess" + "wejh-go/app/utils/fetch" + "wejh-go/config/api/funnelApi" + cbConfig "wejh-go/config/circuitBreaker" ) -var Probe LiveNessProbe -var user *models.User +var Probe *LiveNessProbe func init() { - Probe = LiveNessProbe{ - Config: liveNessConfig.GetLiveNessConfig(), - ApiMap: make(map[string]funnelApi.LoginType), - } - user.OauthPassword = Probe.Config.OauthPassword - user.ZFPassword = Probe.Config.ZFPassword - user.StudentID = Probe.Config.StudentId - go Probe.Start() + Probe = NewLiveNessProbe(cbConfig.GetLiveNessConfig()) } type LiveNessProbe struct { sync.Mutex - Config liveNessConfig.LiveNessProbeConfig - ApiMap map[string]funnelApi.LoginType + ApiMap map[string]funnelApi.LoginType + Duration time.Duration + User *models.User +} + +func NewLiveNessProbe(config cbConfig.LiveNessProbeConfig) *LiveNessProbe { + user := &models.User{ + StudentID: config.StudentId, + OauthPassword: config.OauthPassword, + ZFPassword: config.ZFPassword, + } + return &LiveNessProbe{ + ApiMap: make(map[string]funnelApi.LoginType), + Duration: config.Duration, + User: user, + } } func (l *LiveNessProbe) Add(api string, loginType funnelApi.LoginType) { l.Lock() defer l.Unlock() - l.ApiMap[api] = loginType + l.ApiMap[api+string(loginType)] = loginType } func (l *LiveNessProbe) Remove(key string) { @@ -41,19 +54,60 @@ func (l *LiveNessProbe) Remove(key string) { delete(l.ApiMap, key) } -func (l *LiveNessProbe) Start() { - ticker := time.NewTicker(l.Config.Duration) +func (l *LiveNessProbe) Start(ctx context.Context) { + ticker := time.NewTicker(l.Duration) defer ticker.Stop() for { select { case <-ticker.C: - for api, loginType := range l.ApiMap { - _, err := funnelServices.GetClassTable(user, "2023", "上", api, loginType) - if err == nil { - CB.Success(api) - delete(l.ApiMap, api) + for apiKey, loginType := range l.ApiMap { + api := strings.TrimSuffix(apiKey, string(loginType)) + if err := liveNess(l.User, api, loginType); err == nil { + CB.LB.Add(api, loginType) + CB.Success(api, loginType) + l.Remove(apiKey) } } + case <-ctx.Done(): + return + } + } +} + +func liveNess(u *models.User, api string, loginType funnelApi.LoginType) error { + var password string + if loginType == funnelApi.Oauth { + password = u.OauthPassword + } else { + password = u.ZFPassword + } + form := url.Values{} + form.Add("username", u.StudentID) + form.Add("password", password) + form.Add("type", string(loginType)) + form.Add("year", strconv.Itoa(time.Now().Year()-1)) + form.Add("term", "上") + + f := fetch.Fetch{} + f.Init() + + rc := struct { + Code int `json:"code" binding:"required"` + }{} + for i := 0; i < 5; i++ { + res, err := f.PostForm(api+string(funnelApi.ZFClassTable), form) + if err != nil { + return err + } + if err = json.Unmarshal(res, &rc); err != nil { + return err } + if rc.Code != 413 { + break + } + } + if rc.Code == 200 || rc.Code == 412 || rc.Code == 416 { + return nil } + return apiException.ServerError } diff --git a/app/utils/circuitBreaker/loadbalance.go b/app/utils/circuitBreaker/loadbalance.go index 040b07d..464d940 100644 --- a/app/utils/circuitBreaker/loadbalance.go +++ b/app/utils/circuitBreaker/loadbalance.go @@ -2,14 +2,14 @@ package circuitBreaker import ( "github.com/bytedance/gopkg/lang/fastrand" + "wejh-go/app/apiException" "wejh-go/config/api/funnelApi" ) type LoadBalanceType int const ( - RoundRobin LoadBalanceType = iota - Random + Random LoadBalanceType = iota ) type LoadBalance struct { @@ -17,41 +17,40 @@ type LoadBalance struct { oauthLB *randomLB } -func (lb *LoadBalance) Pick(zfFlag, oauthFlag bool) (string, funnelApi.LoginType) { - var loginType funnelApi.LoginType +func (lb *LoadBalance) Pick(zfFlag, oauthFlag bool) (string, funnelApi.LoginType, error) { + oauthAvailable := oauthFlag && lb.oauthLB.isAvailable() + zfAvailable := zfFlag && lb.zfLB.isAvailable() - if oauthFlag && zfFlag { + if oauthAvailable && zfAvailable { if fastrand.Intn(100) > 50 { - loginType = funnelApi.Oauth - } else { - loginType = funnelApi.ZF + return lb.oauthLB.Pick(), funnelApi.Oauth, nil } - } else if oauthFlag { - loginType = funnelApi.Oauth - } else if zfFlag { - loginType = funnelApi.ZF - } else { - return "", funnelApi.Unknown + return lb.zfLB.Pick(), funnelApi.ZF, nil } - if loginType == funnelApi.Oauth { - return lb.zfLB.Pick(), loginType + + if oauthAvailable { + return lb.oauthLB.Pick(), funnelApi.Oauth, nil } - return lb.oauthLB.Pick(), loginType + if zfAvailable { + return lb.zfLB.Pick(), funnelApi.ZF, nil + } + + return "", funnelApi.Unknown, apiException.NoApiAvailable } -func (lb *LoadBalance) Remove(api string, loginType funnelApi.LoginType) { +func (lb *LoadBalance) Add(api string, loginType funnelApi.LoginType) { if loginType == funnelApi.Oauth { - lb.oauthLB.Remove(api) + lb.oauthLB.Add(api) } else { - lb.zfLB.Remove(api) + lb.zfLB.Add(api) } } -func (lb *LoadBalance) Add(api string, loginType funnelApi.LoginType) { +func (lb *LoadBalance) Remove(api string, loginType funnelApi.LoginType) { if loginType == funnelApi.Oauth { - lb.oauthLB.Add(api) + lb.oauthLB.Remove(api) } else { - lb.zfLB.Add(api) + lb.zfLB.Remove(api) } } @@ -59,7 +58,9 @@ type loadBalance interface { LoadBalance() LoadBalanceType Pick() (api string) ReBalance(apis []string) + Add(api ...string) Remove(api string) + isAvailable() bool } type randomLB struct { @@ -76,6 +77,9 @@ func (b *randomLB) LoadBalance() LoadBalanceType { } func (b *randomLB) Pick() string { + if b.Size == 0 { + return "" + } idx := fastrand.Intn(b.Size) return b.Api[idx] } @@ -96,4 +100,9 @@ func (b *randomLB) Remove(api string) { break } } + b.Size = len(b.Api) +} + +func (b *randomLB) isAvailable() bool { + return b.Size != 0 } diff --git a/config.example.yaml b/config.example.yaml index a2f5bf2..dd09546 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -21,7 +21,15 @@ wechat: # 微信小程序相关配置 (切记不能泄漏) appsecret: funnel: - host: http://api. + host: + +zfCircuit: + studentId: + oauthPassword: + zfPassword: + duration: + apis: + - user: host: diff --git a/config/LiveNess/config.go b/config/LiveNess/config.go deleted file mode 100644 index ef2e3f4..0000000 --- a/config/LiveNess/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package liveNessConfig - -import ( - "github.com/spf13/viper" - "log" - "time" - "wejh-go/config/api/funnelApi" - "wejh-go/config/config" -) - -type LiveNessProbeConfig struct { - StudentId string - OauthPassword string - ZFPassword string - Duration time.Duration -} - -func GetLiveNessConfig() LiveNessProbeConfig { - cfg := LiveNessProbeConfig{} - if config.Config.IsSet("liveNess.studentId") { - cfg.StudentId = config.Config.GetString("liveNess.studentId") - } - if config.Config.IsSet("liveNess.oauthPassword") { - cfg.OauthPassword = config.Config.GetString("liveNess.oauthPassword") - } - if config.Config.IsSet("liveNess.zfPassword") { - cfg.ZFPassword = config.Config.GetString("liveNess.zfPassword") - } - if config.Config.IsSet("liveNess.duration") { - cfg.Duration = config.Config.GetDuration("liveNess.duration") - } - return cfg -} - -type LoadBalanceConfig struct { - Name string `json:"name"` - Url string `json:"url"` - Type funnelApi.LoginType `json:"type"` -} - -func GetLoadBalanceConfig() []LoadBalanceConfig { - var configs []LoadBalanceConfig - err := viper.UnmarshalKey("loadBalance", &configs) - if err != nil { - log.Fatalf("unable to decode into struct, %v", err) - } - return configs -} diff --git a/config/api/funnelApi/api.go b/config/api/funnelApi/api.go index 2d84007..0642036 100644 --- a/config/api/funnelApi/api.go +++ b/config/api/funnelApi/api.go @@ -4,6 +4,6 @@ type LoginType string const ( Oauth LoginType = "OAUTH" - ZF = "ZF" - Unknown = "Unknown" + ZF LoginType = "ZF" + Unknown LoginType = "Unknown" ) diff --git a/config/circuitBreaker/config.go b/config/circuitBreaker/config.go new file mode 100644 index 0000000..a668b72 --- /dev/null +++ b/config/circuitBreaker/config.go @@ -0,0 +1,32 @@ +package circuitBreaker + +import ( + "time" + "wejh-go/config/config" +) + +type LiveNessProbeConfig struct { + StudentId string + OauthPassword string + ZFPassword string + Duration time.Duration +} + +func GetLiveNessConfig() LiveNessProbeConfig { + return LiveNessProbeConfig{ + StudentId: config.Config.GetString("zfCircuit.studentId"), + OauthPassword: config.Config.GetString("zfCircuit.oauthPassword"), + ZFPassword: config.Config.GetString("zfCircuit.zfPassword"), + Duration: config.Config.GetDuration("zfCircuit.duration"), + } +} + +type LoadBalanceConfig struct { + Apis []string +} + +func GetLoadBalanceConfig() LoadBalanceConfig { + return LoadBalanceConfig{ + Apis: config.Config.GetStringSlice("zfCircuit.apis"), + } +} diff --git a/main.go b/main.go index 539fe71..6ca646d 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,18 @@ package main import ( + "context" + "errors" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "log" + "net/http" + "os/signal" + "sync" + "syscall" + "time" "wejh-go/app/midwares" + "wejh-go/app/utils/circuitBreaker" "wejh-go/config/database" "wejh-go/config/router" "wejh-go/config/session" @@ -20,8 +28,37 @@ func main() { session.Init(r) router.Init(r) - err := r.Run() - if err != nil { - log.Fatal("ServerStartFailed", err) + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + var wg sync.WaitGroup + + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + circuitBreaker.Probe.Start(ctx) + }() + + <-ctx.Done() + log.Println("shutting down gracefully") + wg.Wait() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown: ", err) } + + log.Println("Server exiting") }