Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@ -0,0 +1,23 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/main/reply-feed/api:all-srcs",
"//app/service/main/reply-feed/cmd:all-srcs",
"//app/service/main/reply-feed/conf:all-srcs",
"//app/service/main/reply-feed/dao:all-srcs",
"//app/service/main/reply-feed/model:all-srcs",
"//app/service/main/reply-feed/server/grpc:all-srcs",
"//app/service/main/reply-feed/server/http:all-srcs",
"//app/service/main/reply-feed/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,60 @@
# v1.1.9
1. 增加uv展示
# v1.1.8
1. 去掉归一化
# v1.1.7
1. grpc dir
# v1.1.6
1. hour隔天排序
# v1.1.5
1. 修复hour排序
# v1.1.4
1. 新增oid映射
# v1.1.3
1. 新增两个算法, order by like和线上默认逻辑
2. statistics数据不存在时补零
# v1.1.2
1. 新增mid映射
# v1.1.1
1. 数据展示接口,增加小时
# v1.1.0
1. 新增数据展示接口
# v1.0.9
1. fix total_view
# v1.0.8
1. fix mid=0
# v1.0.7
1. add pn count
# v1.0.6
1. res add count
# v1.0.5
1. fix view count
# v1.0.4
1. grpc clinet
# v1.0.3
1. grpc
# v1.0.2
1. 增加white list功能增加管理后台HTTP接口
# v1.0.1
1. 修复init slots的bug
# v1.0.0
1. init

View File

@ -0,0 +1,11 @@
# Owner
caoguoliang
yangjiankun
# Author
yangjiankun
caoguoliang
# Reviewer
caoguoliang
yangjiankun

View File

@ -0,0 +1,14 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- caoguoliang
- yangjiankun
labels:
- main
- service
- service/main/reply-feed
options:
no_parent_owners: true
reviewers:
- caoguoliang
- yangjiankun

View File

@ -0,0 +1,12 @@
# reply-feed-service
# 项目简介
1.
# 编译环境
# 依赖包
# 编译执行

View File

@ -0,0 +1,54 @@
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
proto_library(
name = "v1_proto",
srcs = ["api.proto"],
tags = ["automanaged"],
)
go_proto_library(
name = "v1_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
importpath = "go-common/app/service/main/reply-feed/api",
proto = ":v1_proto",
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["client.go"],
embed = [":v1_go_proto"],
importpath = "go-common/app/service/main/reply-feed/api",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/net/rpc/warden:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
syntax = "proto3";
package community.reply.feed.v1;
option go_package = "v1";
service ReplyFeed {
rpc HotReply (HotReplyReq) returns (HotReplyRes);
rpc Reply (ReplyReq) returns (ReplyRes);
}
message HotReplyReq {
int64 mid = 1;
int64 oid = 2;
int32 tp = 3;
int32 pn = 4;
int32 ps = 5;
}
message HotReplyRes {
string name = 1;
repeated int64 rpIDs = 2[packed=true];
int32 count = 3;
}
message ReplyReq {
int64 mid = 1;
int32 pn = 2;
int32 ps = 3;
}
message ReplyRes {
string name = 1;
}

View File

@ -0,0 +1,22 @@
package v1
import (
"context"
"google.golang.org/grpc"
"go-common/library/net/rpc/warden"
)
// AppID unique app id for service discovery
const AppID = "community.reply.feed"
// NewClient new identify grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (ReplyFeedClient, error) {
client := warden.NewClient(cfg, opts...)
conn, err := client.Dial(context.Background(), "discovery://default/"+AppID)
if err != nil {
return nil, err
}
return NewReplyFeedClient(conn), nil
}

View File

@ -0,0 +1,45 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["test.toml"],
importpath = "go-common/app/service/main/reply-feed/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/reply-feed/conf:go_default_library",
"//app/service/main/reply-feed/server/grpc:go_default_library",
"//app/service/main/reply-feed/server/http:go_default_library",
"//app/service/main/reply-feed/service:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,47 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"go-common/app/service/main/reply-feed/conf"
"go-common/app/service/main/reply-feed/server/grpc"
"go-common/app/service/main/reply-feed/server/http"
"go-common/app/service/main/reply-feed/service"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/net/trace"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
log.Init(conf.Conf.Log)
defer log.Close()
log.Info("reply-feed service start")
trace.Init(conf.Conf.Tracer)
defer trace.Close()
ecode.Init(conf.Conf.Ecode)
svc := service.New(conf.Conf)
http.Init(conf.Conf, svc)
grpc.New(conf.Conf.GRPC, svc)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
svc.Close()
log.Info("reply-feed service exit")
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@ -0,0 +1,66 @@
[midMapping]
"1234"=1
[mysql]
addr = "172.22.34.101:3306"
dsn = "test_3306:UJPZaGKjpb2ylFx3HNhmLuwOYft4MCAi@tcp(172.22.34.101:3306)/bilibili_reply?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8"
active = 20
idle = 2
idleTimeout ="4h"
queryTimeout = "500ms"
execTimeout = "500ms"
tranTimeout = "500ms"
[mysql.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[log]
stdout=true
[redis]
name = "reply-feed-service"
proto = "tcp"
addr = "172.18.33.60:6889"
idle = 10
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "1m"
[redisExpire]
redisReplyZSetExpire = "12h"
[memcache]
name = "reply-feed-service"
proto = "tcp"
addr = "172.18.33.61:11213"
active = 50
idle = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "24h"
[databus]
[databus.event]
key = "170e302355453683"
secret = "3d0e8db7bed0503949e545a469789279"
group = "ReplyFeed-MainCommunity-P"
topic = "ReplyFeed-T"
action ="pub"
name = "reply-feed/event"
proto = "tcp"
addr = "172.18.33.50:6205"
idle = 2
active = 5
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "1h"

View File

@ -0,0 +1,42 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/service/main/reply-feed/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/rpc/warden:go_default_library",
"//library/net/trace:go_default_library",
"//library/queue/databus:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,104 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/sql"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
"go-common/library/net/rpc/warden"
"go-common/library/net/trace"
"go-common/library/queue/databus"
"go-common/library/time"
"github.com/BurntSushi/toml"
)
var (
confPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config .
type Config struct {
Log *log.Config
BM *blademaster.ServerConfig
GRPC *warden.ServerConfig
Verify *verify.Config
Tracer *trace.Config
Redis *redis.Config
RedisExpire *RedisExpire
MySQL *sql.Config
Ecode *ecode.Config
Databus *Databus
MidMapping map[string]int
OidWhiteList map[string]int
}
// RedisExpire RedisExpire
type RedisExpire struct {
RedisReplyZSetExpire time.Duration
}
// Databus databus
type Databus struct {
Event *databus.Config
}
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
// Init init conf
func Init() error {
if confPath != "" {
return local()
}
return remote()
}
func local() (err error) {
_, err = toml.DecodeFile(confPath, &Conf)
return
}
func remote() (err error) {
if client, err = conf.New(); err != nil {
return
}
if err = load(); err != nil {
return
}
go func() {
for range client.Event() {
log.Info("config reload")
if load() != nil {
log.Error("config reload error (%v)", err)
}
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Toml2(); !ok {
return errors.New("load config center error")
}
if _, err = toml.Decode(s, &tmpConf); err != nil {
return errors.New("could not decode config")
}
*Conf = *tmpConf
return
}

View File

@ -0,0 +1,57 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"dao.go",
"db.go",
"redis.go",
],
importpath = "go-common/app/service/main/reply-feed/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/reply-feed/conf:go_default_library",
"//app/service/main/reply-feed/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/xstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"dao_test.go",
"db_test.go",
"redis_test.go",
],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = [
"//app/service/main/reply-feed/conf:go_default_library",
"//app/service/main/reply-feed/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@ -0,0 +1,44 @@
package dao
import (
"context"
"time"
"go-common/app/service/main/reply-feed/conf"
"go-common/library/cache/redis"
xsql "go-common/library/database/sql"
)
// Dao dao
type Dao struct {
c *conf.Config
redis *redis.Pool
redisReplyZSetExpire int
db *xsql.DB
}
// New init mysql db
func New(c *conf.Config) (dao *Dao) {
dao = &Dao{
c: c,
redis: redis.NewPool(c.Redis),
redisReplyZSetExpire: int(time.Duration(c.RedisExpire.RedisReplyZSetExpire) / time.Second),
db: xsql.NewMySQL(c.MySQL),
}
return
}
// Close close the resource.
func (d *Dao) Close() {
d.redis.Close()
d.db.Close()
}
// Ping dao ping
func (d *Dao) Ping(c context.Context) error {
if err := d.PingRedis(c); err != nil {
return err
}
return d.db.Ping(c)
}

View File

@ -0,0 +1,35 @@
package dao
import (
"flag"
"os"
"testing"
"go-common/app/service/main/reply-feed/conf"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.community.reply-feed-service")
flag.Set("conf_token", "b54e90375c747f48f477939fe272eefe")
flag.Set("tree_id", "71072")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "uat")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../cmd/test.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
os.Exit(m.Run())
}

View File

@ -0,0 +1,242 @@
package dao
import (
"context"
"encoding/json"
"errors"
"fmt"
"go-common/app/service/main/reply-feed/model"
"go-common/library/log"
"go-common/library/xstr"
)
const (
_getSlotsStat = "SELECT name,slot,state FROM reply_abtest_strategy"
_getSlotsStatManager = "SELECT name,slot,algorithm,weight,state FROM reply_abtest_strategy"
_setSlot = "UPDATE reply_abtest_strategy SET name=?,algorithm=?,weight=?,state=? WHERE slot IN (%s)"
_setWeight = "UPDATE reply_abtest_strategy SET weight=? WHERE name=?"
_modifyState = "UPDATE reply_abtest_strategy SET state=? WHERE name=?"
_getSlotsStatByName = "SELECT slot,algorithm,weight FROM reply_abtest_strategy WHERE name=?"
_getStatisticsDate = "SELECT name,date,hour,view,total_view,hot_view,hot_click,hot_like,hot_hate,hot_child,hot_report,total_like,total_hate,total_report,total_root,total_child,hot_like_uv,hot_hate_uv,hot_report_uv,hot_child_uv,total_like_uv,total_hate_uv,total_report_uv,total_child_uv,total_root_uv" +
" FROM reply_abtest_statistics WHERE date>=? AND date<=? AND name!='default'"
_upsertLog = "INSERT INTO reply_abtest_statistics (name,date,hour,view,hot_click,hot_view,total_view) VALUES(?,?,?,?,?,?,?)" +
" ON DUPLICATE KEY UPDATE view=view+?,hot_click=hot_click+?,hot_view=hot_view+?,total_view=total_view+?"
)
var (
_countIdleSlot = fmt.Sprintf("SELECT COUNT(*) FROM reply_abtest_strategy WHERE state=1 AND name='%s'", model.DefaultSlotName)
_getIdelSlots = fmt.Sprintf("SELECT slot FROM reply_abtest_strategy WHERE state=1 AND name='%s' ORDER BY slot ASC LIMIT ?", model.DefaultSlotName)
)
/*
SlotsStat
*/
// SlotsMapping get slot name stat from database.
func (d *Dao) SlotsMapping(ctx context.Context) (slotsMap map[string]*model.SlotsMapping, err error) {
slotsMap = make(map[string]*model.SlotsMapping)
rows, err := d.db.Query(ctx, _getSlotsStat)
if err != nil {
log.Error("db.Query(%s) args(%s) error(%v)", _getSlotsStat, err)
return
}
defer rows.Close()
for rows.Next() {
var (
name string
slot int
state int
)
if err = rows.Scan(&name, &slot, &state); err != nil {
log.Error("rows.Scan error(%v)", err)
return
}
mapping, ok := slotsMap[name]
if ok {
mapping.Slots = append(mapping.Slots, slot)
} else {
mapping = &model.SlotsMapping{
Name: name,
Slots: []int{slot},
State: state,
}
}
slotsMap[name] = mapping
}
if err = rows.Err(); err != nil {
log.Error("rows.Err() error(%v)", err)
}
return
}
// SlotsStatManager get slots stat from database, used by manager.
func (d *Dao) SlotsStatManager(ctx context.Context) (s []*model.SlotsStat, err error) {
slotsMap := make(map[string]*model.SlotsStat)
rows, err := d.db.Query(ctx, _getSlotsStatManager)
if err != nil {
log.Error("db.Query(%s) error(%v)", _getSlotsStatManager, err)
return
}
defer rows.Close()
for rows.Next() {
var (
name, algorithm, weight string
slot, state int
)
if err = rows.Scan(&name, &slot, &algorithm, &weight, &state); err != nil {
log.Error("rows.Scan error(%v)", err)
return
}
if mapping, ok := slotsMap[name]; ok {
mapping.Slots = append(mapping.Slots, slot)
} else {
slotsMap[name] = &model.SlotsStat{
Name: name,
Slots: []int{slot},
Algorithm: algorithm,
Weight: weight,
State: state,
}
}
}
if err = rows.Err(); err != nil {
log.Error("rows.Err() error(%v)", err)
return
}
for _, stat := range slotsMap {
s = append(s, stat)
}
return
}
// CountIdleSlot count idle slot which name="default" and state=1
func (d *Dao) CountIdleSlot(ctx context.Context) (count int, err error) {
if err = d.db.QueryRow(ctx, _countIdleSlot).Scan(&count); err != nil {
log.Error("db.QueryRow() error(%v)", err)
}
return
}
// IdleSlots get idle slots
func (d *Dao) IdleSlots(ctx context.Context, count int) (slots []int64, err error) {
rows, err := d.db.Query(ctx, _getIdelSlots, count)
if err != nil {
log.Error("db.Query(%s) args(%d) error(%v)", _getIdelSlots, count, err)
return
}
defer rows.Close()
for rows.Next() {
var slot int64
if err = rows.Scan(&slot); err != nil {
log.Error("rows.Scan() error(%v)", err)
return
}
slots = append(slots, slot)
}
// 槽位不够新创建实验组
if len(slots) < count {
slots = nil
err = errors.New("out of slot")
return
}
if err = rows.Err(); err != nil {
log.Error("rows.Err() error(%v)", err)
}
return
}
// ModifyState ModifyState
func (d *Dao) ModifyState(ctx context.Context, name string, state int) (err error) {
if _, err = d.db.Exec(ctx, _modifyState, state, name); err != nil {
log.Error("db.Exec(%s) args(%d, %s) error(%v)", _modifyState, state, name, err)
}
return
}
// UpdateSlotsStat UpdateSlotStat and set state inactive.
func (d *Dao) UpdateSlotsStat(ctx context.Context, name, algorithm, weight string, slots []int64, state int) (err error) {
if _, err = d.db.Exec(ctx, fmt.Sprintf(_setSlot, xstr.JoinInts(slots)), name, algorithm, weight, state); err != nil {
log.Error("db.Exec() error(%v)", err)
}
return
}
// SlotsStatByName get slots stat by name.
func (d *Dao) SlotsStatByName(ctx context.Context, name string) (slots []int64, algorithm, weight string, err error) {
rows, err := d.db.Query(ctx, _getSlotsStatByName, name)
if err != nil {
log.Error("db.Query(%s) args(%s) error(%v)", _getSlotsStatByName, name, err)
return
}
defer rows.Close()
for rows.Next() {
var (
slot int64
)
if err = rows.Scan(&slot, &algorithm, &weight); err != nil {
log.Error("rows.Scan() error(%v)", err)
return
}
slots = append(slots, slot)
}
if err = rows.Err(); err != nil {
log.Error("rows.Err() error(%v)", err)
}
return
}
// UpdateWeight update a test set weight by name and algorithm.
func (d *Dao) UpdateWeight(ctx context.Context, name string, weight interface{}) (err error) {
b, err := json.Marshal(weight)
if err != nil {
return
}
if _, err = d.db.Exec(ctx, _setWeight, string(b), name); err != nil {
log.Error("db.Exec(%s), error(%v)", _setWeight, err)
}
return
}
/*
Statistics
*/
// UpsertStatistics insert or update statistics from database, if err != nil, retry
func (d *Dao) UpsertStatistics(ctx context.Context, name string, date int, hour int, s *model.StatisticsStat) (err error) {
if _, err = d.db.Exec(ctx, _upsertLog,
name, date, hour,
s.View, s.HotClick, s.HotView, s.TotalView,
s.View, s.HotClick, s.HotView, s.TotalView); err != nil {
return
}
return
}
// StatisticsByDate StatisticsByDate
func (d *Dao) StatisticsByDate(ctx context.Context, begin, end int64) (stats model.StatisticsStats, err error) {
rows, err := d.db.Query(ctx, _getStatisticsDate, begin, end)
if err != nil {
log.Error("db.Query(%s) args(%d, %d) error(%v)", _getStatisticsDate, begin, end, err)
return
}
defer rows.Close()
for rows.Next() {
var s = new(model.StatisticsStat)
err = rows.Scan(&s.Name, &s.Date, &s.Hour, &s.View, &s.TotalView, &s.HotView, &s.HotClick, &s.HotLike, &s.HotHate, &s.HotChildReply,
&s.HotReport, &s.TotalLike, &s.TotalHate, &s.TotalReport, &s.TotalRootReply, &s.TotalChildReply,
&s.HotLikeUV, &s.HotHateUV, &s.HotReportUV, &s.HotChildUV, &s.TotalLikeUV, &s.TotalHateUV, &s.TotalReportUV, &s.TotalChildUV, &s.TotalRootUV)
if err != nil {
log.Error("rows.Scan() error(%v)", err)
return
}
stats = append(stats, s)
}
if err = rows.Err(); err != nil {
log.Error("rows.Err() error(%v)", err)
return
}
return
}

View File

@ -0,0 +1,158 @@
package dao
import (
"context"
"testing"
"go-common/app/service/main/reply-feed/model"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoSlotsMapping(t *testing.T) {
convey.Convey("SlotsMapping", t, func(ctx convey.C) {
ctx.Convey("When everything goes positive", func(ctx convey.C) {
slotsMap, err := d.SlotsMapping(context.Background())
ctx.Convey("Then err should be nil.slotsMap should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(slotsMap, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoSlotsStatManager(t *testing.T) {
convey.Convey("SlotsStatManager", t, func(ctx convey.C) {
ctx.Convey("When everything goes positive", func(ctx convey.C) {
s, err := d.SlotsStatManager(context.Background())
ctx.Convey("Then err should be nil.s should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(s, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoIdleSlot(t *testing.T) {
convey.Convey("IdleSlot", t, func(ctx convey.C) {
var (
count = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
slots, err := d.IdleSlots(context.Background(), count)
ctx.Convey("Then err should be nil.slots should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(slots, convey.ShouldBeNil)
})
})
})
}
func TestDaoCountIdleSlot(t *testing.T) {
convey.Convey("CountIdleSlot", t, func(ctx convey.C) {
ctx.Convey("When everything goes positive", func(ctx convey.C) {
count, err := d.CountIdleSlot(context.Background())
ctx.Convey("Then err should be nil. count should greater than 0.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(count, convey.ShouldBeGreaterThan, 0)
})
})
})
}
func TestDaoModifyState(t *testing.T) {
convey.Convey("ModifyState", t, func(ctx convey.C) {
var (
name = ""
state = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.ModifyState(context.Background(), name, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoUpdateSlotsStat(t *testing.T) {
convey.Convey("UpdateSlotsStat", t, func(ctx convey.C) {
var (
name = ""
algorithm = ""
weight = ""
slots = []int64{-1}
state = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.UpdateSlotsStat(context.Background(), name, algorithm, weight, slots, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestSlotsStatByName(t *testing.T) {
convey.Convey("SlotsStatByName", t, func(ctx convey.C) {
var (
name = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
slots, algorithm, weight, err := d.SlotsStatByName(context.Background(), name)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(slots, convey.ShouldBeNil)
ctx.So(algorithm, convey.ShouldEqual, "")
ctx.So(weight, convey.ShouldEqual, "")
})
})
})
}
func TestDaoUpdateWeight(t *testing.T) {
convey.Convey("UpdateWeight", t, func(ctx convey.C) {
var (
name = ""
weight = interface{}(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.UpdateWeight(context.Background(), name, weight)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoUpsertStatistics(t *testing.T) {
convey.Convey("UpsertStatistics", t, func(ctx convey.C) {
var (
name = ""
date = int(0)
hour = int(0)
s = &model.StatisticsStat{}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.UpsertStatistics(context.Background(), name, date, hour, s)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoStatisticsByDate(t *testing.T) {
convey.Convey("StatisticsByDate", t, func(ctx convey.C) {
var (
begin = int64(0)
end = int64(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
stats, err := d.StatisticsByDate(context.Background(), begin, end)
ctx.Convey("Then err should be nil.stats should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(stats, convey.ShouldNotBeNil)
})
})
})
}

View File

@ -0,0 +1,68 @@
package dao
import (
"context"
"fmt"
"go-common/library/cache/redis"
"go-common/library/log"
)
const (
// r_<实验组名>_<oid>_<type>
// 用redis ZSet存储热门评论列表score为热评分数member为rpID
_replyZSetFormat = "r_%s_%d_%d"
)
func keyReplyZSet(name string, oid int64, tp int) string {
return fmt.Sprintf(_replyZSetFormat, name, oid, tp)
}
// PingRedis redis health check.
func (d *Dao) PingRedis(ctx context.Context) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
_, err = conn.Do("SET", "ping", "pong")
return
}
// ExpireReplyZSetRds expire reply list.
func (d *Dao) ExpireReplyZSetRds(ctx context.Context, name string, oid int64, tp int) (ok bool, err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
key := keyReplyZSet(name, oid, tp)
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.redisReplyZSetExpire)); err != nil {
if err == redis.ErrNil {
err = nil
return
}
log.Error("redis EXPIRE key(%s) error(%v)", key, err)
}
return
}
// ReplyZSetRds get reply list from redis sorted set.
func (d *Dao) ReplyZSetRds(ctx context.Context, name string, oid int64, tp, start, end int) (rpIDs []int64, err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
key := keyReplyZSet(name, oid, tp)
values, err := redis.Values(conn.Do("ZREVRANGE", key, start, end))
if err != nil {
log.Error("redis ZREVRANGE(%s, %d, %d) error(%v)", key, start, end, err)
return
}
if err = redis.ScanSlice(values, &rpIDs); err != nil {
log.Error("redis ScanSlice(%v) error(%v)", values, err)
}
return
}
// CountReplyZSetRds count reply num.
func (d *Dao) CountReplyZSetRds(ctx context.Context, name string, oid int64, tp int) (count int, err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
key := keyReplyZSet(name, oid, tp)
if count, err = redis.Int(conn.Do("ZCARD", key)); err != nil {
log.Error("conn.Do(ZCARD, %s) error(%v)", key, err)
}
return
}

View File

@ -0,0 +1,88 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaokeyReplyZSet(t *testing.T) {
convey.Convey("keyReplyZSet", t, func(ctx convey.C) {
var (
name = ""
oid = int64(0)
tp = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := keyReplyZSet(name, oid, tp)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoPingRedis(t *testing.T) {
convey.Convey("PingRedis", t, func(ctx convey.C) {
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.PingRedis(context.Background())
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoExpireReplyZSetRds(t *testing.T) {
convey.Convey("ExpireReplyZSetRds", t, func(ctx convey.C) {
var (
name = ""
oid = int64(0)
tp = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
ok, err := d.ExpireReplyZSetRds(context.Background(), name, oid, tp)
ctx.Convey("Then err should be nil.ok should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(ok, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetReplyZSetRds(t *testing.T) {
convey.Convey("ReplyZSetRds", t, func(ctx convey.C) {
var (
name = ""
oid = int64(0)
tp = int(0)
start = int(0)
end = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
rpIDs, err := d.ReplyZSetRds(context.Background(), name, oid, tp, start, end)
ctx.Convey("Then err should be nil.rpIDs should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(rpIDs, convey.ShouldBeNil)
})
})
})
}
func TestDaoCountReplyZSetRds(t *testing.T) {
convey.Convey("CountReplyZSetRds", t, func(ctx convey.C) {
var (
name = ""
oid = int64(-1)
tp = int(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
count, err := d.CountReplyZSetRds(context.Background(), name, oid, tp)
ctx.Convey("Then err should be nil.rpIDs should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(count, convey.ShouldEqual, 0)
})
})
})
}

View File

@ -0,0 +1,29 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["model.go"],
importpath = "go-common/app/service/main/reply-feed/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//library/ecode:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,236 @@
package model
import (
"go-common/library/ecode"
"sort"
)
// const var
const (
WilsonLHRRAlgorithm = "wilsonLHRR"
WilsonLHRRFluidAlgorithm = "wilsonLHRRFluid"
OriginAlgorithm = "origin"
LikeDescAlgorithm = "likeDesc"
StateInactive = int(0)
StateActive = int(1)
SlotsNum = 100
DefaultSlotName = "default"
DefaultAlgorithm = "default"
DefaultWeight = ""
)
// EventMsg EventMsg
type EventMsg struct {
Action string `json:"action"`
Oid int64 `json:"oid"`
Tp int `json:"tp"`
}
// SlotsMapping slot name mapping
type SlotsMapping struct {
Name string
Slots []int
State int
}
// SlotsStat slots stat
type SlotsStat struct {
Name string
Slots []int
Algorithm string
Weight string
State int
}
// StatisticsStats StatisticsStats
type StatisticsStats []*StatisticsStat
// GroupByName group statistics by name
func (s StatisticsStats) GroupByName() (res map[string]StatisticsStats) {
res = make(map[string]StatisticsStats)
for _, stat := range s {
if _, ok := res[stat.Name]; ok {
res[stat.Name] = append(res[stat.Name], stat)
} else {
var tmp []*StatisticsStat
tmp = append(tmp, stat)
res[stat.Name] = tmp
}
}
return
}
// StatisticsStat 实验组或者对照组的各项统计
type StatisticsStat struct {
// 流量所属槽位 0~99
Slot int
// 所属实验组名
Name string
State int
Date int
Hour int
HotLike int64
HotHate int64
HotReport int64
HotChildReply int64
// 整个评论区
TotalLike int64
TotalHate int64
TotalReport int64
TotalChildReply int64
TotalRootReply int64
// 用户点开评论区次数
View uint32
// 评论列表接口调用次数
TotalView uint32
// 热门评论接口调用次数
HotView uint32
// 更多热门评论点击次数
HotClick uint32
// 用户在评论首页看到的热门评论被点赞点踩评论以及举报的次数
// UV的统计数据
HotLikeUV int64
HotHateUV int64
HotReportUV int64
HotChildUV int64
TotalLikeUV int64
TotalHateUV int64
TotalReportUV int64
TotalChildUV int64
TotalRootUV int64
}
// Merge Merge
func (stat1 *StatisticsStat) Merge(stat2 *StatisticsStat) (stat3 *StatisticsStat) {
stat3 = new(StatisticsStat)
stat3.View = stat1.View + stat2.View
stat3.HotView = stat1.HotView + stat2.HotView
stat3.HotClick = stat1.HotClick + stat2.HotClick
stat3.TotalView = stat1.TotalView + stat2.TotalView
return
}
// DivideByPercent ...
func (stat1 *StatisticsStat) DivideByPercent(percent int64) (stat2 *StatisticsStat) {
stat2 = new(StatisticsStat)
if percent <= 0 {
return
}
stat2.Name = stat1.Name
stat2.Date = stat1.Date
stat2.Hour = stat1.Hour
stat2.View = stat1.View / uint32(percent)
stat2.HotView = stat1.HotView / uint32(percent)
stat2.HotClick = stat1.HotClick / uint32(percent)
stat2.TotalView = stat1.TotalView / uint32(percent)
stat2.HotLike = stat1.HotLike / percent
stat2.HotHate = stat1.HotHate / percent
stat2.HotChildReply = stat1.HotChildReply / percent
stat2.HotReport = stat1.HotReport / percent
stat2.TotalLike = stat1.TotalLike / percent
stat2.TotalHate = stat1.TotalHate / percent
stat2.TotalReport = stat1.TotalReport / percent
stat2.TotalRootReply = stat1.TotalRootReply / percent
stat2.TotalChildReply = stat1.TotalChildReply / percent
return
}
// MergeByDate MergeByDate
func (stat1 *StatisticsStat) MergeByDate(stat2 *StatisticsStat) (stat3 *StatisticsStat) {
stat3 = new(StatisticsStat)
stat3.Name = stat1.Name
stat3.Date = stat1.Date
stat3.View = stat1.View + stat2.View
stat3.HotView = stat1.HotView + stat2.HotView
stat3.HotClick = stat1.HotClick + stat2.HotClick
stat3.TotalView = stat1.TotalView + stat2.TotalView
stat3.HotLike = stat1.HotLike + stat2.HotLike
stat3.HotHate = stat1.HotHate + stat2.HotHate
stat3.HotChildReply = stat1.HotChildReply + stat2.HotChildReply
stat3.HotReport = stat1.HotReport + stat2.HotReport
stat3.TotalLike = stat1.TotalLike + stat2.TotalLike
stat3.TotalHate = stat1.TotalHate + stat2.TotalHate
stat3.TotalReport = stat1.TotalReport + stat2.TotalReport
stat3.TotalRootReply = stat1.TotalRootReply + stat2.TotalRootReply
stat3.TotalChildReply = stat1.TotalChildReply + stat2.TotalChildReply
return
}
// WilsonLHRRWeight wilson score interval weight
type WilsonLHRRWeight struct {
Like float64 `json:"like"`
Hate float64 `json:"hate"`
Reply float64 `json:"reply"`
Report float64 `json:"report"`
}
// Validate Validate
func (weight WilsonLHRRWeight) Validate() (err error) {
if weight.Report*weight.Reply*weight.Hate*weight.Like <= 0 {
err = ecode.RequestErr
return
}
return
}
// WilsonLHRRFluidWeight WilsonLHRRFluidWeight
type WilsonLHRRFluidWeight struct {
Like float64 `json:"like"`
Hate float64 `json:"hate"`
Reply float64 `json:"reply"`
Report float64 `json:"report"`
Slope float64 `json:"slope"`
}
// Validate Validate
func (weight WilsonLHRRFluidWeight) Validate() (err error) {
if weight.Report*weight.Reply*weight.Hate*weight.Like*weight.Slope <= 0 {
err = ecode.RequestErr
return
}
return
}
// SSReq ss req
type SSReq struct {
DateFrom int64 `form:"date_from" validate:"required"`
DateEnd int64 `form:"date_end" validate:"required"`
Hour bool `form:"hour"`
}
// SSHourRes ss res
type SSHourRes struct {
Legend []string `json:"legend"`
XAxis []string `json:"x_axis"`
Series map[string][]*StatisticsStat `json:"series"`
}
// Sort ...
func (s *SSHourRes) Sort() {
sort.Strings(s.Legend)
sort.Strings(s.XAxis)
for _, v := range s.Series {
sort.Slice(v, func(i, j int) bool { return v[i].Date*100+v[i].Hour < v[j].Date*100+v[j].Hour })
}
}
// SSDateRes ss res
type SSDateRes struct {
Legend []string `json:"legend"`
XAxis []int `json:"x_axis"`
Series map[string][]*StatisticsStat `json:"series"`
}
// Sort ...
func (s *SSDateRes) Sort() {
sort.Strings(s.Legend)
sort.Ints(s.XAxis)
for _, v := range s.Series {
sort.Slice(v, func(i, j int) bool { return v[i].Date < v[j].Date })
}
}

View File

@ -0,0 +1,33 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["server.go"],
importpath = "go-common/app/service/main/reply-feed/server/grpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/reply-feed/api:go_default_library",
"//app/service/main/reply-feed/service:go_default_library",
"//library/net/rpc/warden:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,19 @@
package grpc
import (
pb "go-common/app/service/main/reply-feed/api"
"go-common/app/service/main/reply-feed/service"
"go-common/library/net/rpc/warden"
)
// New grpc server
func New(cfg *warden.ServerConfig, srv *service.Service) (wsvr *warden.Server) {
var err error
wsvr = warden.NewServer(cfg)
pb.RegisterReplyFeedServer(wsvr.Server(), srv)
wsvr, err = wsvr.Start()
if err != nil {
panic(err)
}
return
}

View File

@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["http.go"],
importpath = "go-common/app/service/main/reply-feed/server/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/reply-feed/conf:go_default_library",
"//app/service/main/reply-feed/model:go_default_library",
"//app/service/main/reply-feed/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,253 @@
package http
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"go-common/app/service/main/reply-feed/conf"
"go-common/app/service/main/reply-feed/model"
"go-common/app/service/main/reply-feed/service"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
svc *service.Service
vfy *verify.Verify
)
// Init init
func Init(c *conf.Config, s *service.Service) {
svc = s
vfy = verify.New(c.Verify)
engine := bm.DefaultServer(c.BM)
router(engine)
if err := engine.Start(); err != nil {
log.Error("engine.Start error(%v)", err)
panic(err)
}
}
func router(e *bm.Engine) {
e.Ping(ping)
e.Register(register)
g := e.Group("/x/reply-feed")
{
g.POST("/strategy/new", newEGroup)
g.POST("/strategy/edit", editEgroup)
g.POST("/strategy/state", modifyState)
g.POST("/strategy/resize", resizeSlots)
g.POST("/strategy/reset", resetEgroup)
g.GET("/strategy/list", listEGroup)
g.GET("/statistics/list", statistics)
}
}
func ping(c *bm.Context) {
if err := svc.Ping(c); err != nil {
log.Error("ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}
func register(c *bm.Context) {
c.JSON(map[string]interface{}{}, nil)
}
func validate(algorithm string, weight string) (err error) {
switch algorithm {
case model.WilsonLHRRAlgorithm:
w := &model.WilsonLHRRWeight{}
if err = json.Unmarshal([]byte(weight), &w); err != nil {
return
}
return w.Validate()
case model.WilsonLHRRFluidAlgorithm:
w := &model.WilsonLHRRFluidWeight{}
if err = json.Unmarshal([]byte(weight), &w); err != nil {
return
}
return w.Validate()
case model.OriginAlgorithm, model.LikeDescAlgorithm:
return
default:
log.Error("unknown algorithm accepted (%s)", algorithm)
err = ecode.RequestErr
}
if err != nil {
return
}
return
}
func listEGroup(c *bm.Context) {
stats, err := svc.SlotStatsManager(c)
if err != nil {
c.JSON(nil, err)
return
}
c.JSON(stats, nil)
}
func newEGroup(c *bm.Context) {
v := new(struct {
Name string `form:"name" validate:"required"`
Percent int `form:"percent" validate:"required"`
Algorithm string `form:"algorithm" validate:"required"`
Weight string `form:"weight" validate:"required"`
})
var (
err error
)
if err = c.Bind(v); err != nil {
return
}
if err = validate(v.Algorithm, v.Weight); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, svc.NewEGroup(c, v.Name, v.Algorithm, v.Weight, v.Percent))
}
func editEgroup(c *bm.Context) {
v := new(struct {
Name string `form:"name" validate:"required"`
Algorithm string `form:"algorithm" validate:"required"`
Weight string `form:"weight" validate:"required"`
Slots []int64 `form:"slots,split" validate:"required"`
})
var (
err error
)
if err = c.Bind(v); err != nil {
return
}
if err = validate(v.Algorithm, v.Weight); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, svc.EditSlotsStat(c, v.Name, v.Algorithm, v.Weight, v.Slots))
}
func modifyState(c *bm.Context) {
v := new(struct {
Name string `form:"name" validate:"required"`
State int `form:"state"`
})
var (
err error
)
if err = c.Bind(v); err != nil {
return
}
c.JSON(nil, svc.ModifyState(c, v.Name, v.State))
}
func resizeSlots(c *bm.Context) {
v := new(struct {
Name string `form:"name" validate:"required"`
Percent int `form:"percent" validate:"required"`
})
var (
err error
)
if err = c.Bind(v); err != nil {
return
}
c.JSON(nil, svc.ResizeSlots(c, v.Name, v.Percent))
}
func resetEgroup(c *bm.Context) {
v := new(struct {
Name string `form:"name" validate:"required"`
})
var (
err error
)
if err = c.Bind(v); err != nil {
return
}
c.JSON(nil, svc.ResetEGroup(c, v.Name))
}
func statistics(c *bm.Context) {
v := new(model.SSReq)
if err := c.Bind(v); err != nil {
return
}
if v.Hour {
res := new(model.SSHourRes)
data, err := svc.StatisticsByHour(c, v)
if err != nil {
c.JSON(nil, err)
return
}
xAxisMap := make(map[string]struct{})
res.Series = make(map[string][]*model.StatisticsStat)
for legend, m := range data {
res.Legend = append(res.Legend, legend)
for xAxis := range m {
xAxisMap[xAxis] = struct{}{}
}
}
for k := range xAxisMap {
res.XAxis = append(res.XAxis, k)
}
for _, legend := range res.Legend {
if hourStatistics, ok := data[legend]; ok {
for _, hour := range res.XAxis {
if stat, exists := hourStatistics[hour]; exists {
res.Series[legend] = append(res.Series[legend], stat)
} else {
t := strings.Split(hour, "-")
if len(t) < 2 {
c.Abort()
return
}
date, _ := strconv.Atoi(t[0])
hour, _ := strconv.Atoi(t[1])
res.Series[legend] = append(res.Series[legend], &model.StatisticsStat{Date: date, Hour: hour})
}
}
}
}
res.Sort()
c.JSON(res, nil)
return
}
res := new(model.SSDateRes)
data, err := svc.StatisticsByDate(c, v)
if err != nil {
c.JSON(nil, err)
return
}
xAxisMap := make(map[int]struct{})
res.Series = make(map[string][]*model.StatisticsStat)
for legend, m := range data {
res.Legend = append(res.Legend, legend)
for xAxis := range m {
xAxisMap[xAxis] = struct{}{}
}
}
for k := range xAxisMap {
res.XAxis = append(res.XAxis, k)
}
for _, legend := range res.Legend {
if dateStatistics, ok := data[legend]; ok {
for _, date := range res.XAxis {
if stat, exists := dateStatistics[date]; exists {
res.Series[legend] = append(res.Series[legend], stat)
} else {
res.Series[legend] = append(res.Series[legend], &model.StatisticsStat{Date: date})
}
}
}
}
res.Sort()
c.JSON(res, nil)
}

View File

@ -0,0 +1,41 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"admin.go",
"reply.go",
"service.go",
],
importpath = "go-common/app/service/main/reply-feed/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/reply-feed/api:go_default_library",
"//app/service/main/reply-feed/conf:go_default_library",
"//app/service/main/reply-feed/dao:go_default_library",
"//app/service/main/reply-feed/model:go_default_library",
"//library/log:go_default_library",
"//library/net/netutil:go_default_library",
"//vendor/github.com/robfig/cron:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,144 @@
package service
import (
"context"
"errors"
"fmt"
"go-common/app/service/main/reply-feed/model"
"go-common/library/log"
)
/*
SlotsMapping
*/
// SlotStatsManager SlotStatsManager
func (s *Service) SlotStatsManager(ctx context.Context) ([]*model.SlotsStat, error) {
return s.dao.SlotsStatManager(ctx)
}
// NewEGroup new experimental group.
func (s *Service) NewEGroup(ctx context.Context, name, algorithm, weight string, percent int) (err error) {
var (
usedSlots []int64
slots []int64
)
if usedSlots, _, _, err = s.dao.SlotsStatByName(ctx, name); err != nil {
return
}
if len(usedSlots) > 0 {
return errors.New("duplicate name")
}
if slots, err = s.dao.IdleSlots(ctx, percent); err != nil {
return
}
return s.dao.UpdateSlotsStat(ctx, name, algorithm, weight, slots, model.StateInactive)
}
// ResizeSlots resize slots
func (s *Service) ResizeSlots(ctx context.Context, name string, percent int) (err error) {
var (
count int
slots []int64
idelSlots []int64
algorithm, weight string
)
if slots, algorithm, weight, err = s.dao.SlotsStatByName(ctx, name); err != nil {
return
}
if percent > len(slots) {
if count, err = s.dao.CountIdleSlot(ctx); err != nil {
return
}
if percent > count {
err = errors.New("out of slot")
return
}
if idelSlots, err = s.dao.IdleSlots(ctx, percent-len(slots)); err != nil {
return
}
if err = s.dao.UpdateSlotsStat(ctx, name, algorithm, weight, idelSlots, model.StateActive); err != nil {
return
}
} else {
if err = s.dao.UpdateSlotsStat(ctx, model.DefaultSlotName, model.DefaultAlgorithm, model.DefaultWeight, slots[percent:], model.StateActive); err != nil {
return
}
}
return
}
// EditSlotsStat edit a test set weight.
func (s *Service) EditSlotsStat(ctx context.Context, name, algorithm, weight string, slots []int64) (err error) {
if err = s.dao.UpdateSlotsStat(ctx, name, algorithm, weight, slots, model.StateInactive); err != nil {
log.Error("Edit SlotsMapping Failed, Error (%v)", err)
}
return
}
// ModifyState modify test set state, activate or inactivate by name.
func (s *Service) ModifyState(ctx context.Context, name string, state int) (err error) {
return s.dao.ModifyState(ctx, name, state)
}
// ResetEGroup ResetEGroup
func (s *Service) ResetEGroup(ctx context.Context, name string) (err error) {
var slots []int64
if slots, _, _, err = s.dao.SlotsStatByName(ctx, name); err != nil {
return
}
if err = s.dao.UpdateSlotsStat(ctx, model.DefaultSlotName, model.DefaultAlgorithm, model.DefaultWeight, slots, model.StateActive); err != nil {
return
}
return
}
/*
TODO(Statistics)
Statistics
*/
// StatisticsByDate GetStatisticsByDate
func (s *Service) StatisticsByDate(ctx context.Context, req *model.SSReq) (res map[string]map[int]*model.StatisticsStat, err error) {
res = make(map[string]map[int]*model.StatisticsStat)
stats, err := s.dao.StatisticsByDate(ctx, req.DateFrom, req.DateEnd)
if err != nil {
return
}
groupedStatistics := stats.GroupByName()
for name, ss := range groupedStatistics {
dateStatistics := make(map[int]*model.StatisticsStat)
for _, s := range ss {
if _, ok := dateStatistics[s.Date]; ok {
dateStatistics[s.Date] = dateStatistics[s.Date].MergeByDate(s)
} else {
dateStatistics[s.Date] = s
}
}
res[name] = dateStatistics
}
return
}
// StatisticsByHour GetStatisticsByHour
func (s *Service) StatisticsByHour(ctx context.Context, req *model.SSReq) (res map[string]map[string]*model.StatisticsStat, err error) {
res = make(map[string]map[string]*model.StatisticsStat)
stats, err := s.dao.StatisticsByDate(ctx, req.DateFrom, req.DateEnd)
if err != nil {
return
}
groupedStatistics := stats.GroupByName()
for name, ss := range groupedStatistics {
hourStatistics := make(map[string]*model.StatisticsStat)
for _, s := range ss {
if s.Hour < 10 {
hourStatistics[fmt.Sprintf("%d-0%d", s.Date, s.Hour)] = s
} else {
hourStatistics[fmt.Sprintf("%d-%d", s.Date, s.Hour)] = s
}
}
res[name] = hourStatistics
}
return
}

View File

@ -0,0 +1,106 @@
package service
import (
"context"
"sync/atomic"
"go-common/app/service/main/reply-feed/api"
"go-common/app/service/main/reply-feed/model"
)
func (s *Service) name(mid int64) (string, bool) {
if mid == 0 {
return model.DefaultSlotName, false
}
slot, ok := s.midMapping[mid]
s.statisticsLock.RLock()
defer s.statisticsLock.RUnlock()
if ok && slot >= 0 && slot < model.SlotsNum {
return s.statisticsStats[slot].Name, true
}
stat := s.statisticsStats[mid%model.SlotsNum]
if stat.Name == model.DefaultSlotName || stat.State == model.StateInactive {
return model.DefaultSlotName, false
}
return stat.Name, true
}
func (s *Service) incrHot(req *v1.HotReplyReq) {
if req.Mid == 0 {
return
}
if req.Pn == 1 {
if req.Ps == 20 {
// 用户点击更多热评
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].HotClick, 1)
} else {
// 点开评论区自带的5条热门评论
return
}
}
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].HotView, 1)
}
func (s *Service) incrTotalView(req *v1.ReplyReq) {
if req.Mid == 0 {
return
}
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].TotalView, 1)
}
func (s *Service) incrView(req *v1.ReplyReq) {
if req.Mid == 0 {
return
}
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].View, 1)
}
// HotReply return hot reply
func (s *Service) HotReply(ctx context.Context, req *v1.HotReplyReq) (res *v1.HotReplyRes, err error) {
var (
start = (req.Pn - 1) * req.Ps
end = start + req.Ps - 1
ok bool
count int
)
res = new(v1.HotReplyRes)
// increment hot view and hot click count
s.incrHot(req)
if tp, exists := s.oidWhiteList[req.Oid]; exists && int32(tp) == req.Tp {
res.Name = model.DefaultSlotName
return
}
res.Name, ok = s.name(req.Mid)
if !ok {
return
}
if ok, err = s.dao.ExpireReplyZSetRds(ctx, res.Name, req.Oid, int(req.Tp)); err != nil {
return
}
if ok {
if res.RpIDs, err = s.dao.ReplyZSetRds(ctx, res.Name, req.Oid, int(req.Tp), int(start), int(end)); err != nil {
return
}
if count, err = s.dao.CountReplyZSetRds(ctx, res.Name, req.Oid, int(req.Tp)); err != nil {
return
}
res.Count = int32(count)
} else {
// s.eventProducer.Send(ctx, strconv.FormatInt(req.Oid, 10), &model.EventMsg{Action: "re_idx", Oid: req.Oid, Tp: int(req.Tp)})
res.Name = model.DefaultSlotName
}
return
}
// Reply do increment reply view count.
func (s *Service) Reply(ctx context.Context, req *v1.ReplyReq) (res *v1.ReplyRes, err error) {
res = new(v1.ReplyRes)
// 用户点开评论区的次数
if req.Pn == 1 {
s.incrView(req)
}
// 用户在评论区总浏览次数
s.incrTotalView(req)
res.Name, _ = s.name(req.Mid)
return
}

View File

@ -0,0 +1,171 @@
package service
import (
"context"
"strconv"
"sync"
"time"
"go-common/app/service/main/reply-feed/conf"
"go-common/app/service/main/reply-feed/dao"
"go-common/app/service/main/reply-feed/model"
"go-common/library/log"
"go-common/library/net/netutil"
"github.com/robfig/cron"
)
// Service struct
type Service struct {
c *conf.Config
dao *dao.Dao
// backoff
bc netutil.BackoffConfig
cron *cron.Cron
statisticsStats []model.StatisticsStat
statisticsLock sync.RWMutex
// eventProducer *databus.Databus
midMapping map[int64]int
oidWhiteList map[int64]int
}
// New init
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
dao: dao.New(c),
bc: netutil.BackoffConfig{
MaxDelay: 1 * time.Second,
BaseDelay: 100 * time.Millisecond,
Factor: 1.6,
Jitter: 0.2,
},
cron: cron.New(),
statisticsStats: make([]model.StatisticsStat, model.SlotsNum),
// eventProducer: databus.New(c.Databus.Event),
midMapping: make(map[int64]int),
oidWhiteList: make(map[int64]int),
}
// toml不支持int为key
for k, v := range s.c.MidMapping {
mid, err := strconv.ParseInt(k, 10, 64)
if err != nil {
continue
}
s.midMapping[mid] = v
}
for k, v := range s.c.OidWhiteList {
oid, err := strconv.ParseInt(k, 10, 64)
if err != nil {
continue
}
s.oidWhiteList[oid] = v
}
// 初始化各个实验组所占流量槽位
var err error
if err = s.loadSlots(); err != nil {
panic(err)
}
go s.loadproc()
// 每整小时执行一次将统计数据写入DB
s.cron.AddFunc("@hourly", func() {
s.persistStatistics()
})
s.cron.Start()
return s
}
// namePercent ...
// func (s *Service) namePercent() map[string]int64 {
// p := make(map[string]int64)
// s.statisticsLock.RLock()
// for _, slot := range s.statisticsStats {
// if _, ok := p[slot.Name]; ok {
// p[slot.Name]++
// } else {
// p[slot.Name] = 1
// }
// }
// s.statisticsLock.RUnlock()
// return p
// }
func (s *Service) loadproc() {
for {
time.Sleep(time.Minute)
s.loadSlots()
}
}
func (s *Service) loadSlots() (err error) {
slotsMapping, err := s.dao.SlotsMapping(context.Background())
if err != nil {
return
}
s.statisticsLock.Lock()
for _, mapping := range slotsMapping {
for _, slot := range mapping.Slots {
s.statisticsStats[slot].Name = mapping.Name
s.statisticsStats[slot].Slot = slot
s.statisticsStats[slot].State = mapping.State
}
}
s.statisticsLock.Unlock()
log.Warn("statistics stat (%v)", s.statisticsStats)
return
}
func (s *Service) persistStatistics() {
ctx := context.Background()
statisticsMap := make(map[string]*model.StatisticsStat)
s.statisticsLock.RLock()
for _, stat := range s.statisticsStats {
s, ok := statisticsMap[stat.Name]
if ok {
statisticsMap[stat.Name] = s.Merge(&stat)
} else {
statisticsMap[stat.Name] = &stat
}
}
s.statisticsLock.RUnlock()
now := time.Now()
year, month, day := now.Date()
date := year*10000 + int(month)*100 + day
hour := now.Hour()
for name, stat := range statisticsMap {
err := s.dao.UpsertStatistics(ctx, name, date, hour, stat)
var (
retryTimes = 0
maxRetryTimes = 5
)
for err != nil && retryTimes < maxRetryTimes {
time.Sleep(s.bc.Backoff(retryTimes))
err = s.dao.UpsertStatistics(ctx, name, date, hour, stat)
retryTimes++
}
if retryTimes >= maxRetryTimes {
log.Error("upsert statistics error retry reached max retry times.")
}
}
for i := range s.statisticsStats {
reset(&s.statisticsStats[i])
}
}
func reset(stat *model.StatisticsStat) {
stat.HotView = 0
stat.View = 0
stat.HotClick = 0
stat.TotalView = 0
}
// Ping Service
func (s *Service) Ping(ctx context.Context) (err error) {
return s.dao.Ping(ctx)
}
// Close Service
func (s *Service) Close() {
s.persistStatistics()
s.dao.Close()
}