go-common/app/job/openplatform/article/service/service.go
2019-04-22 18:49:16 +08:00

617 lines
16 KiB
Go

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)
}
}