You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
616 lines
16 KiB
616 lines
16 KiB
package service |
|
|
|
import ( |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"sync" |
|
"time" |
|
|
|
tagrpc "go-common/app/interface/main/tag/rpc/client" |
|
artmdl "go-common/app/interface/openplatform/article/model" |
|
artrpc "go-common/app/interface/openplatform/article/rpc/client" |
|
"go-common/app/job/openplatform/article/conf" |
|
"go-common/app/job/openplatform/article/dao" |
|
"go-common/app/job/openplatform/article/model" |
|
thumdl "go-common/app/service/main/thumbup/model" |
|
"go-common/library/conf/env" |
|
"go-common/library/log" |
|
"go-common/library/log/infoc" |
|
"go-common/library/queue/databus" |
|
"go-common/library/stat/prom" |
|
) |
|
|
|
const ( |
|
_sharding = 100 // goroutines for dealing the stat |
|
_chanSize = 10240 |
|
_articleTable = "filtered_articles" // article table name |
|
_authorTable = "article_authors" // 作者表 |
|
) |
|
|
|
type lastTimeStat struct { |
|
time int64 |
|
stat *artmdl.StatMsg |
|
} |
|
|
|
// Service . |
|
type Service struct { |
|
c *conf.Config |
|
dao *dao.Dao |
|
waiter *sync.WaitGroup |
|
closed bool |
|
// monitor *monitor.Service |
|
articleSub *databus.Databus |
|
articleStatSub *databus.Databus |
|
likeStatSub *databus.Databus |
|
replyStatSub *databus.Databus |
|
favoriteStatSub *databus.Databus |
|
coinStatSub *databus.Databus |
|
articleRPC *artrpc.Service |
|
tagRPC *tagrpc.Service |
|
statCh [_sharding]chan *artmdl.StatMsg |
|
statLastTime [_sharding]map[int64]*lastTimeStat |
|
categoriesMap map[int64]*artmdl.Category |
|
categoriesReverseMap map[int64][]*artmdl.Category |
|
sitemapMap map[int64]struct{} |
|
urlListHead *urlNode |
|
urlListTail *urlNode |
|
lastURLNode *urlNode |
|
sitemapXML string |
|
likeCh chan *thumdl.StatMsg |
|
updateDbInterval int64 |
|
updateSortInterval time.Duration |
|
cheatInfoc *infoc.Infoc |
|
// *Cnt means sum of consumed messages. |
|
statCnt, binCnt int64 |
|
cheatArts map[int64]int |
|
sortLimitTime int64 |
|
setting *model.Setting |
|
// infoc |
|
logCh chan interface{} |
|
} |
|
|
|
// New creates a Service instance. |
|
func New(c *conf.Config) (s *Service) { |
|
s = &Service{ |
|
c: c, |
|
dao: dao.New(c), |
|
waiter: new(sync.WaitGroup), |
|
// monitor: monitor.New(), |
|
articleSub: databus.New(c.ArticleSub), |
|
articleStatSub: databus.New(c.ArticleStatSub), |
|
replyStatSub: databus.New(c.ReplyStatSub), |
|
favoriteStatSub: databus.New(c.FavoriteStatSub), |
|
coinStatSub: databus.New(c.CoinStatSub), |
|
likeStatSub: databus.New(c.LikeStatSub), |
|
articleRPC: artrpc.New(c.ArticleRPC), |
|
tagRPC: tagrpc.New2(c.TagRPC), |
|
updateDbInterval: int64(time.Duration(c.Job.UpdateDbInterval) / time.Second), |
|
updateSortInterval: time.Duration(c.Job.UpdateSortInterval), |
|
likeCh: make(chan *thumdl.StatMsg, 1e4), |
|
cheatInfoc: infoc.New(c.CheatInfoc), |
|
categoriesMap: make(map[int64]*artmdl.Category), |
|
categoriesReverseMap: make(map[int64][]*artmdl.Category), |
|
sitemapMap: make(map[int64]struct{}), |
|
sortLimitTime: int64(time.Duration(c.Job.SortLimitTime) / time.Second), |
|
logCh: make(chan interface{}, 1024), |
|
} |
|
// s.monitor.SetConfig(c.HTTPClient) |
|
for i := int64(0); i < _sharding; i++ { |
|
i := i |
|
// for stat |
|
s.statCh[i] = make(chan *artmdl.StatMsg, _chanSize) |
|
s.statLastTime[i] = make(map[int64]*lastTimeStat) |
|
s.waiter.Add(1) |
|
go s.statproc(i) |
|
} |
|
s.loadCategories() |
|
s.loadSettings() |
|
// 10 go routines with WaitGroup |
|
s.waiter.Add(10) |
|
go s.consumeStat() |
|
go s.consumeCanal() |
|
go s.checkConsumer() |
|
go s.retryStat() |
|
go s.retryReply() |
|
go s.retryGame() |
|
go s.retryFlow() |
|
go s.retryDynamic() |
|
go s.retryCDN() |
|
go s.retryCache() |
|
// other go routines |
|
go s.updateSortproc() |
|
go s.activityLikeproc() |
|
go s.updateReadCountproc() |
|
go s.updateHotspotsproc() |
|
go s.updateCheatArtsproc() |
|
go s.loadCategoriesproc() |
|
go s.loadSettingsproc() |
|
go s.recommendAuthorproc() |
|
go s.checkReadStatusProc() |
|
go s.infocproc() |
|
go s.sitemapproc() |
|
return |
|
} |
|
|
|
// consumeStat consumes article's stat. |
|
func (s *Service) consumeStat() { |
|
defer s.waiter.Done() |
|
for { |
|
if s.closed { |
|
for i := 0; i < _sharding; i++ { |
|
close(s.statCh[i]) |
|
} |
|
log.Info("databus: article-job stat consumer exit!") |
|
return |
|
} |
|
L: |
|
select { |
|
case msg, ok := <-s.articleStatSub.Messages(): |
|
if !ok { |
|
break L |
|
} |
|
s.statCnt++ |
|
msg.Commit() |
|
sm := &artmdl.StatMsg{} |
|
if err := json.Unmarshal(msg.Value, sm); err != nil { |
|
log.Error("json.Unmarshal(%s) error(%+v)", msg.Value, err) |
|
dao.PromError("service:解析计数databus消息") |
|
break L |
|
} |
|
if sm.Aid <= 0 { |
|
log.Warn("aid(%d) <=0 message(%s)", sm.Aid, msg.Value) |
|
break L |
|
} |
|
key := sm.Aid % _sharding |
|
s.statCh[key] <- sm |
|
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key]))) |
|
log.Info("consumeStat key:%s partition:%d offset:%d msg: %v)", msg.Key, msg.Partition, msg.Offset, sm.String()) |
|
case msg, ok := <-s.likeStatSub.Messages(): |
|
if !ok { |
|
break L |
|
} |
|
msg.Commit() |
|
like := &thumdl.StatMsg{} |
|
if err := json.Unmarshal(msg.Value, like); err != nil { |
|
log.Error("json.Unmarshal(%s) error(%+v)", msg.Value, err) |
|
dao.PromError("service:解析like计数databus消息") |
|
break L |
|
} |
|
if like.Type != "article" { |
|
continue |
|
} |
|
select { |
|
case s.likeCh <- like: |
|
default: |
|
} |
|
key := like.ID % _sharding |
|
s.statCh[key] <- &artmdl.StatMsg{Like: &like.Count, Dislike: &like.DislikeCount, Aid: like.ID} |
|
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key]))) |
|
log.Info("consumeLikeStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, like) |
|
case msg, ok := <-s.replyStatSub.Messages(): |
|
if !ok { |
|
break L |
|
} |
|
msg.Commit() |
|
m := &thumdl.StatMsg{} |
|
if err := json.Unmarshal(msg.Value, m); err != nil { |
|
log.Error("reply json.Unmarshal(%s) error(%+v)", msg.Value, err) |
|
dao.PromError("service:解析reply计数databus消息") |
|
break L |
|
} |
|
if m.Type != "article" { |
|
continue |
|
} |
|
key := m.ID % _sharding |
|
s.statCh[key] <- &artmdl.StatMsg{Reply: &m.Count, Aid: m.ID} |
|
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key]))) |
|
log.Info("consumeReplyStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, m) |
|
case msg, ok := <-s.favoriteStatSub.Messages(): |
|
if !ok { |
|
break L |
|
} |
|
msg.Commit() |
|
m := &thumdl.StatMsg{} |
|
if err := json.Unmarshal(msg.Value, m); err != nil { |
|
log.Error("favorite json.Unmarshal(%s) error(%+v)", msg.Value, err) |
|
dao.PromError("service:解析favorite计数databus消息") |
|
break L |
|
} |
|
if m.Type != "article" { |
|
continue |
|
} |
|
key := m.ID % _sharding |
|
s.statCh[key] <- &artmdl.StatMsg{Favorite: &m.Count, Aid: m.ID} |
|
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key]))) |
|
log.Info("consumeFavoriteStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, m) |
|
case msg, ok := <-s.coinStatSub.Messages(): |
|
if !ok { |
|
break L |
|
} |
|
msg.Commit() |
|
m := &thumdl.StatMsg{} |
|
if err := json.Unmarshal(msg.Value, m); err != nil { |
|
log.Error("coin json.Unmarshal(%s) error(%+v)", msg.Value, err) |
|
dao.PromError("service:解析coin计数databus消息") |
|
break L |
|
} |
|
if m.Type != "article" { |
|
continue |
|
} |
|
key := m.ID % _sharding |
|
s.statCh[key] <- &artmdl.StatMsg{Coin: &m.Count, Aid: m.ID} |
|
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key]))) |
|
log.Info("consumeCoinStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, m) |
|
} |
|
} |
|
} |
|
|
|
// consumeCanal consumes article's binlog databus. |
|
func (s *Service) consumeCanal() { |
|
defer s.waiter.Done() |
|
var c = context.TODO() |
|
for { |
|
msg, ok := <-s.articleSub.Messages() |
|
if !ok { |
|
log.Info("databus: article-job binlog consumer exit!") |
|
return |
|
} |
|
s.binCnt++ |
|
msg.Commit() |
|
m := &model.Message{} |
|
if err := json.Unmarshal(msg.Value, m); err != nil { |
|
log.Error("json.Unmarshal(%s) error(%+v)", msg.Value, err) |
|
continue |
|
} |
|
switch m.Table { |
|
case _articleTable: |
|
s.upArticles(c, m.Action, m.New, m.Old) |
|
case _authorTable: |
|
s.upAuthors(c, m.Action, m.New, m.Old) |
|
} |
|
log.Info("consumeCanal key:%s partition:%d offset:%d", msg.Key, msg.Partition, msg.Offset) |
|
} |
|
} |
|
|
|
func (s *Service) retryStat() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
bs []byte |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
bs, err = s.dao.PopStat(c) |
|
if err != nil || bs == nil { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
msg := &dao.StatRetry{} |
|
if err = json.Unmarshal(bs, msg); err != nil { |
|
log.Error("json.Unmarshal(%s) error(%+v)", bs, err) |
|
dao.PromError("service:解析计数重试消息") |
|
continue |
|
} |
|
if msg.Count > dao.RetryStatCount { |
|
continue |
|
} |
|
log.Info("retry: %s", bs) |
|
switch msg.Action { |
|
case dao.RetryUpdateStatCache: |
|
if err = s.updateCache(c, msg.Data, msg.Count+1); err != nil { |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
dao.PromInfo("service:重试计数更新缓存") |
|
case dao.RetryUpdateStatDB: |
|
if err = s.updateDB(c, msg.Data, msg.Count+1); err != nil { |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
dao.PromInfo("service:重试计数更新DB") |
|
} |
|
} |
|
} |
|
|
|
func (s *Service) retryReply() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
aid, mid int64 |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
aid, mid, err = s.dao.PopReply(c) |
|
if err != nil || aid == 0 || mid == 0 { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
log.Info("retry reply: aid(%d) mid(%d)", aid, mid) |
|
if err = s.openReply(c, aid, mid); err != nil { |
|
log.Error("s.openReply(%d,%d) error(%+v)", aid, mid, err) |
|
dao.PromInfo("service:重试打开评论区") |
|
time.Sleep(100 * time.Millisecond) |
|
continue |
|
} |
|
log.Info("s.openReply(%d,%d) retry success", aid, mid) |
|
} |
|
} |
|
|
|
func (s *Service) retryCDN() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
file string |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
file, err = s.dao.PopCDN(c) |
|
if err != nil || file == "" { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
log.Info("retry CDN: file(%s)", file) |
|
if err = s.dao.PurgeCDN(c, file); err != nil { |
|
log.Error("s.dao.PurgeCDN(%s) error(%+v)", file, err) |
|
dao.PromInfo("service:刷新CDN重试") |
|
time.Sleep(100 * time.Millisecond) |
|
continue |
|
} |
|
log.Info("s.dao.PurgeCDN(%s) retry success.", file) |
|
} |
|
} |
|
|
|
func (s *Service) retryCache() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
bs []byte |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
bs, err = s.dao.PopArtCache(c) |
|
if err != nil || bs == nil { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
msg := &dao.CacheRetry{} |
|
if err = json.Unmarshal(bs, msg); err != nil { |
|
log.Error("json.Unmarshal(%s) error(%+v)", bs, err) |
|
dao.PromError("service:解析文章缓存重试消息") |
|
continue |
|
} |
|
log.Info("retry cache: %s", bs) |
|
switch msg.Action { |
|
case dao.RetryAddArtCache: |
|
if err = s.addArtCache(c, msg.Aid); err != nil { |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
dao.PromInfo("service:重试添加文章缓存") |
|
case dao.RetryUpdateArtCache: |
|
if err = s.updateArtCache(c, msg.Aid, msg.Cid); err != nil { |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
dao.PromInfo("service:重试更新文章缓存") |
|
case dao.RetryDeleteArtCache: |
|
if err = s.deleteArtCache(c, msg.Aid, msg.Mid); err != nil { |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
dao.PromInfo("service:重试删除文章缓存") |
|
case dao.RetryDeleteArtRecCache: |
|
if err = s.deleteArtRecommendCache(c, msg.Aid, msg.Cid); err != nil { |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
dao.PromInfo("service:重试删除文章推荐缓存") |
|
} |
|
} |
|
} |
|
|
|
// checkConsumer checks consumer state. |
|
func (s *Service) checkConsumer() { |
|
defer s.waiter.Done() |
|
if env.DeployEnv != env.DeployEnvProd { |
|
return |
|
} |
|
var ( |
|
// ctx = context.TODO() |
|
// c* means sum of consumed messages. |
|
c1, c2 int64 |
|
) |
|
for { |
|
time.Sleep(5 * time.Hour) |
|
if s.statCnt-c1 == 0 { |
|
// msg := "databus: article-job stat did not consume within a minute" |
|
// s.monitor.Sms(ctx, s.c.SMS.Phone, s.c.SMS.Token, msg) |
|
// log.Warn(msg) |
|
log.Warn("databus: article-job stat did not consume within a minute") |
|
} |
|
c1 = s.statCnt |
|
if s.binCnt-c2 == 0 { |
|
// msg := "databus: article-job binlog did not consume within a minute" |
|
// s.monitor.Sms(ctx, s.c.SMS.Phone, s.c.SMS.Token, msg) |
|
// log.Warn(msg) |
|
log.Warn("databus: article-job binlog did not consume within a minute") |
|
} |
|
c2 = s.binCnt |
|
} |
|
} |
|
|
|
// Ping reports the heath of services. |
|
func (s *Service) Ping(c context.Context) (err error) { |
|
return s.dao.Ping(c) |
|
} |
|
|
|
// Close releases resources which owned by the Service instance. |
|
func (s *Service) Close() (err error) { |
|
defer s.waiter.Wait() |
|
s.articleSub.Close() |
|
s.articleStatSub.Close() |
|
s.closed = true |
|
log.Info("article-job has been closed.") |
|
return |
|
} |
|
|
|
func (s *Service) retryGame() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
info *model.GameCacheRetry |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
info, err = s.dao.PopGameCache(c) |
|
if (err != nil) || (info == nil) { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
for { |
|
log.Info("retry_game: %+v", info) |
|
dao.PromInfo("service:重试同步游戏") |
|
if err = s.dao.GameSync(c, info.Action, info.Aid); err != nil { |
|
time.Sleep(time.Millisecond * 100) |
|
continue |
|
} |
|
break |
|
} |
|
log.Info("s.GameSync(%s, aid: %d) retry success", info.Action, info.Aid) |
|
} |
|
} |
|
|
|
func (s *Service) activityLikeproc() { |
|
var c = context.TODO() |
|
for { |
|
msg, ok := <-s.likeCh |
|
if !ok { |
|
log.Info("activityLikeproc: exit!") |
|
return |
|
} |
|
s.dao.LikeSync(c, msg.ID, msg.Count) |
|
} |
|
} |
|
|
|
func (s *Service) retryFlow() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
info *model.FlowCacheRetry |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
info, err = s.dao.PopFlowCache(c) |
|
if (err != nil) || (info == nil) { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
for { |
|
log.Info("retry_flow: %+v", info) |
|
dao.PromInfo("service:重试同步flow") |
|
if err = s.dao.FlowSync(c, info.Mid, info.Aid); err != nil { |
|
time.Sleep(time.Second * 1) |
|
continue |
|
} |
|
break |
|
} |
|
log.Info("s.FlowSync(mid:%v, aid: %d) retry success", info.Mid, info.Aid) |
|
} |
|
} |
|
|
|
func (s *Service) retryDynamic() { |
|
defer s.waiter.Done() |
|
var ( |
|
err error |
|
info *model.DynamicCacheRetry |
|
c = context.TODO() |
|
) |
|
for { |
|
if s.closed { |
|
return |
|
} |
|
info, err = s.dao.PopDynamicCache(c) |
|
if (err != nil) || (info == nil) { |
|
time.Sleep(time.Second) |
|
continue |
|
} |
|
for { |
|
log.Info("retry_dynamic: %+v", info) |
|
dao.PromInfo("service:重试同步dynamic") |
|
if err = s.dao.PubDynamic(c, info.Mid, info.Aid, info.Show, info.Comment, info.Ts, info.DynamicIntro); err != nil { |
|
time.Sleep(time.Second * 1) |
|
continue |
|
} |
|
break |
|
} |
|
log.Info("s.PubDynamic(mid:%v, aid: %d) retry success %+v", info.Mid, info.Aid, info) |
|
} |
|
} |
|
|
|
func (s *Service) updateReadCountproc() { |
|
var c = context.TODO() |
|
for { |
|
err := s.articleRPC.RebuildAllListReadCount(c) |
|
if err != nil { |
|
log.Error("s.updateReadCountproc() err: %+v", err) |
|
time.Sleep(time.Second * 5) |
|
continue |
|
} |
|
log.Info("s.updateReadCountproc() success") |
|
dao.PromError("更新文集阅读数") |
|
time.Sleep(time.Duration(s.c.Job.ListReadCountInterval)) |
|
} |
|
} |
|
|
|
func (s *Service) updateHotspotsproc() { |
|
var c = context.TODO() |
|
var lastUpdate int64 |
|
for { |
|
var force bool |
|
duration := int64(time.Duration(s.c.Job.HotspotForceInterval) / time.Second) |
|
if (time.Now().Unix() - lastUpdate) > duration { |
|
force = true |
|
} |
|
err := s.articleRPC.UpdateHotspots(c, &artmdl.ArgForce{Force: force}) |
|
if err != nil { |
|
log.Error("s.UpdateHotspots() err: %+v", err) |
|
dao.PromError("更新热点运营文章") |
|
time.Sleep(time.Second * 5) |
|
continue |
|
} |
|
if force { |
|
lastUpdate = time.Now().Unix() |
|
} |
|
log.Info("s.UpdateHotspots() success force:%v", force) |
|
time.Sleep(time.Duration(s.c.Job.HotspotInterval)) |
|
} |
|
} |
|
|
|
func (s *Service) updateCheatArtsproc() { |
|
var c = context.TODO() |
|
for { |
|
arts, err := s.dao.CheatArts(c) |
|
if err != nil { |
|
log.Error("s.updateCheatArtsproc() err: %+v", err) |
|
dao.PromError("更新反作弊文章列表") |
|
time.Sleep(time.Second * 5) |
|
continue |
|
} |
|
log.Info("s.updateCheatArtsproc() success, len: %v", len(arts)) |
|
s.cheatArts = arts |
|
time.Sleep(time.Minute) |
|
} |
|
}
|
|
|