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,22 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/openplatform/abtest/cmd:all-srcs",
"//app/service/openplatform/abtest/conf:all-srcs",
"//app/service/openplatform/abtest/dao:all-srcs",
"//app/service/openplatform/abtest/http:all-srcs",
"//app/service/openplatform/abtest/model:all-srcs",
"//app/service/openplatform/abtest/service:all-srcs",
],
tags = ["automanaged"],
)

View File

@@ -0,0 +1,11 @@
##### Version 0.1.0
> 1. 使用sync map优化读取性能
##### Version 0.0.2
> 1. 内部自身调用,去除鉴权服务
> 2. 添加abtest管理端的分组功能
##### Version 0.0.1
> 1. A/B 实验 后台服务

View File

@@ -0,0 +1,10 @@
# Owner
lijiadong
qiuliang
# Author
lijiadong
luhao
# Reviewer
qiuliang

View File

@@ -0,0 +1,16 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- lijiadong
- luhao
- qiuliang
labels:
- openplatform
- service
- service/openplatform/abtest
options:
no_parent_owners: true
reviewers:
- lijiadong
- luhao
- qiuliang

View File

@@ -0,0 +1,13 @@
## open-abtest-service
#### 项目简介
> 1.open-abtest服务
#### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
#### 依赖包
> 1.公共包go-common
#### 特别说明
> 1.model目录可能会被其他项目引用请谨慎更改并通知各方。

View File

@@ -0,0 +1,42 @@
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 = ["open-abtest.toml"],
importpath = "go-common/app/service/openplatform/abtest/cmd",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/http:go_default_library",
"//app/service/openplatform/abtest/service: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,48 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/service/openplatform/abtest/conf"
"go-common/app/service/openplatform/abtest/http"
"go-common/app/service/openplatform/abtest/service"
"go-common/library/log"
"go-common/library/net/trace"
)
func main() {
flag.Parse()
// init conf,log,trace,stat,perf.
if err := conf.Init(); err != nil {
panic(err)
}
log.Init(conf.Conf.Log)
defer log.Close()
trace.Init(nil)
defer trace.Close()
// service init
svr := service.New(conf.Conf)
http.Init(conf.Conf, svr)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("abtest-service get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
time.Sleep(time.Second * 2)
log.Info("abtest-service exit")
return
case syscall.SIGHUP:
// TODO reload
default:
return
}
}
}

View File

@@ -0,0 +1,96 @@
# This is a TOML document. Boom.
[log]
dir = "/data/log/abtest"
[db]
[db.ab]
addr = "172.16.33.203:3306"
dsn = "root:123456@tcp(172.16.33.203:3306)/tickets?timeout=5s&readTimeout=30s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 5
idle = 5
queryTimeout = "1s"
execTimeout = "1s"
tranTimeout = "2s"
[db.ab.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[bm]
[bm.inner]
addr = "0.0.0.0:8801"
timeout = "2s"
[bm.outer]
addr = "0.0.0.0:8802"
timeout = "2s"
[redis]
name = "abtest-service"
proto = "tcp"
addr = "172.16.33.203:6379"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "24h"
verifyCdTimes = "2h"
[identify]
whiteAccessKey = ""
whiteMid = 0
csrfOn = false
[identify.app]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[httpClient]
[httpClient.read]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
dial = "1s"
timeout = "4s"
keepAlive = "60s"
timer = 1000
[httpClient.read.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[httpClient.write]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
dial = "1s"
timeout = "3s"
keepAlive = "60s"
timer = 1000
[httpClient.write.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100

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 = ["conf.go"],
importpath = "go-common/app/service/openplatform/abtest/conf",
tags = ["automanaged"],
deps = [
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/trace: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,115 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/sql"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/trace"
xtime "go-common/library/time"
"github.com/BurntSushi/toml"
)
// Conf global variable.
var (
Conf = &Config{}
client *conf.Client
confPath string
)
// Config struct of conf.
type Config struct {
// base
// log
Log *log.Config
// db
DB *DB
// redis
Redis *Redis
// http
BM *HTTPServers
// http client
HTTPClient HTTPClient
// tracer
Tracer *trace.Config
//stat
Stat int
Stra int
}
// DB db config.
type DB struct {
Ab *sql.Config
}
// Redis conf.
type Redis struct {
*redis.Config
Expire xtime.Duration
VerifyCdTimes xtime.Duration
}
// HTTPServers Http Servers
type HTTPServers struct {
Inner *bm.ServerConfig
Outer *bm.ServerConfig
}
// HTTPClient config
type HTTPClient struct {
Read *bm.ClientConfig
Write *bm.ClientConfig
}
func outer() (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 event")
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Value("open-abtest.toml"); !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
}
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
// Init int config
func Init() error {
if confPath != "" {
return outer()
}
return remote()
}

View File

@@ -0,0 +1,60 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"abtest_test.go",
"dao_test.go",
"group_test.go",
"redis_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/model:go_default_library",
"//library/cache/redis:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"abtest.go",
"dao.go",
"group.go",
"redis.go",
],
importpath = "go-common/app/service/openplatform/abtest/dao",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors: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,225 @@
package dao
import (
"context"
xsql "database/sql"
"encoding/json"
"fmt"
"go-common/app/service/openplatform/abtest/model"
"go-common/library/cache/redis"
"go-common/library/database/sql"
"github.com/pkg/errors"
)
const (
_selAllByStatus = "SELECT `id`,`name`,`desc`,`stra`,`seed`,`result`,`status`,`version`,`group`,`author_name`,`modifier_name`,`ctime`,`mtime` FROM `abtest` WHERE `deleted`=0 AND `status` IN (%s) ORDER BY mtime DESC LIMIT %d, %d"
_selAllByStatusAndGroup = "SELECT `id`,`name`,`desc`,`stra`,`seed`,`result`,`status`,`version`,`group`,`author_name`,`modifier_name`,`ctime`,`mtime` FROM `abtest` WHERE `group`=? AND `deleted`=0 AND `status` IN (%s) ORDER BY mtime DESC LIMIT %d, %d"
_selActByGroup = "SELECT `id`,`name`,`desc`,`stra`,`seed`,`result`,`status`,`version`,`group`,`author_name`,`modifier_name`,`ctime`,`mtime` FROM `abtest` WHERE `group`=? AND `status`=1 AND `deleted`=0"
_selCnt = "SELECT count(*) as `count` FROM `abtest` WHERE `deleted`=0 AND `status` IN (%s)"
_selCntByGroup = "SELECT count(*) as `count` FROM `abtest` WHERE `group`=? AND `deleted`=0 AND `status` IN (%s)"
_selByID = "SELECT `id`,`name`,`desc`,`stra`,`seed`,`result`,`status`,`version`,`group`,`author_name`,`modifier_name`,`ctime`,`mtime` FROM `abtest` WHERE `id`=? AND `deleted`=0"
_selByIDAndGroup = "SELECT `id`,`name`,`desc`,`stra`,`seed`,`result`,`status`,`version`,`group`,`author_name`,`modifier_name`,`ctime`,`mtime` FROM `abtest` WHERE `id`=? AND `group`=? AND `deleted`=0"
_insAB = "INSERT INTO `abtest` (`name`,`desc`,`stra`,`seed`,`result`,`status`,`group`,`author_name`,`modifier_name`) VALUES(?,?,?,?,?,0,?,?,?)"
_upAB = "UPDATE `abtest` SET `name`=?,`desc`=?,`stra`=?,`result`=?,`modifier_name`=?,`version`=? WHERE `id`=?"
_upStatus = "UPDATE `abtest` SET `status`=?,`modifier_name`=? WHERE `id`=?"
_delAB = "UPDATE `abtest` SET `deleted`=1 WHERE `id`=? AND `status`!=1"
)
//ActByGroup 根据group获取当前激活项目
func (d *Dao) ActByGroup(c context.Context, group int) (res []*model.AB, err error) {
var (
rows *sql.Rows
straStr string
)
if rows, err = d.db.Query(c, _selActByGroup, group); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ActByGroup] d.db.Query err: %v", err))
return
}
defer rows.Close()
res = make([]*model.AB, 0)
for rows.Next() {
ele := &model.AB{}
if err = rows.Scan(&ele.ID, &ele.Name, &ele.Desc, &straStr, &ele.Seed, &ele.Result, &ele.Status, &ele.Version, &ele.Group, &ele.Author, &ele.Modifier, &ele.CreateTime, &ele.ModifyTime); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ActByGroup] rows.Scan err: %v", err))
return
}
if err = json.Unmarshal([]byte(straStr), &ele.Stra); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ActByGroup] json.Unmarshal err: %v", err))
return
}
res = append(res, ele)
}
return
}
//Ab 获取单个测试
func (d *Dao) Ab(c context.Context, id int) (res *model.AB, err error) {
var straStr string
row := d.db.QueryRow(c, _selByID, id)
res = &model.AB{}
if err = row.Scan(&res.ID, &res.Name, &res.Desc, &straStr, &res.Seed, &res.Result, &res.Status, &res.Version, &res.Group, &res.Author, &res.Modifier, &res.CreateTime, &res.ModifyTime); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|Ab] row.Scan err: %v", err))
return
}
err = json.Unmarshal([]byte(straStr), &res.Stra)
return
}
//AbByIDAndGroup 获取单个测试
func (d *Dao) AbByIDAndGroup(c context.Context, id int, group int) (res *model.AB, err error) {
var (
straStr string
row *sql.Row
)
row = d.db.QueryRow(c, _selByIDAndGroup, id, group)
res = &model.AB{}
if err = row.Scan(&res.ID, &res.Name, &res.Desc, &straStr, &res.Seed, &res.Result, &res.Status, &res.Version, &res.Group, &res.Author, &res.Modifier, &res.CreateTime, &res.ModifyTime); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|AbByIDAndGroup] row.Scan err: %v", err))
return
}
err = json.Unmarshal([]byte(straStr), &res.Stra)
return
}
//CountAb 获取测试数量
func (d *Dao) CountAb(c context.Context, mstatus string) (count int, err error) {
row := d.db.QueryRow(c, fmt.Sprintf(_selCnt, mstatus))
if err = row.Scan(&count); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|CountAb] row.Scan err: %v", err))
return
}
return
}
//CountAbByGroup 获取测试数量
func (d *Dao) CountAbByGroup(c context.Context, mstatus string, group int) (count int, err error) {
row := d.db.QueryRow(c, fmt.Sprintf(_selCntByGroup, mstatus), group)
if err = row.Scan(&count); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|CountAbByGroup] row.Scan err: %v", err))
return
}
return
}
//ListAb 分页获取所有测试列表
func (d *Dao) ListAb(c context.Context, offset, size int, mstatus string) (res []*model.AB, count int, err error) {
var (
rows *sql.Rows
straStr string
)
if rows, err = d.db.Query(c, fmt.Sprintf(_selAllByStatus, mstatus, offset, size)); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ListAb] d.db.Query err: %v", err))
return
}
defer rows.Close()
res = make([]*model.AB, 0)
for rows.Next() {
ele := &model.AB{}
if err = rows.Scan(&ele.ID, &ele.Name, &ele.Desc, &straStr, &ele.Seed, &ele.Result, &ele.Status, &ele.Version, &ele.Group, &ele.Author, &ele.Modifier, &ele.CreateTime, &ele.ModifyTime); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ListAb] rows.Scan err: %v", err))
return
}
err = json.Unmarshal([]byte(straStr), &ele.Stra)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ListAb] json.Unmarshal err: %v", err))
continue
}
res = append(res, ele)
}
count, err = d.CountAb(c, mstatus)
return
}
//ListAbByGroup 分页获取分组测试列表
func (d *Dao) ListAbByGroup(c context.Context, offset, size int, mstatus string, group int) (res []*model.AB, count int, err error) {
var (
rows *sql.Rows
straStr string
)
if rows, err = d.db.Query(c, fmt.Sprintf(_selAllByStatusAndGroup, mstatus, offset, size), group); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ListAbByGroup] d.db.Query err: %v", err))
return
}
defer rows.Close()
res = make([]*model.AB, 0)
for rows.Next() {
ele := &model.AB{}
if err = rows.Scan(&ele.ID, &ele.Name, &ele.Desc, &straStr, &ele.Seed, &ele.Result, &ele.Status, &ele.Version, &ele.Group, &ele.Author, &ele.Modifier, &ele.CreateTime, &ele.ModifyTime); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ListAbByGroup] rows.Scan err: %v", err))
return
}
err = json.Unmarshal([]byte(straStr), &ele.Stra)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.abtest|ListAbByGroup] json.Unmarshal err: %v", err))
continue
}
res = append(res, ele)
}
count, err = d.CountAbByGroup(c, mstatus, group)
return
}
//AddAb 添加AB实验
func (d *Dao) AddAb(c context.Context, name, desc string, stra string, seed, result, group int, username string) (newID int64, err error) {
var res xsql.Result
if res, err = d.db.Exec(c, _insAB, name, desc, stra, seed, result, group, username, username); err != nil {
return
}
newID, err = res.LastInsertId()
return
}
//DelAb 删除AB实验
func (d *Dao) DelAb(c context.Context, id int) (rowsAffected int64, err error) {
var res xsql.Result
if res, err = d.db.Exec(c, _delAB, id); err != nil {
return
}
return res.RowsAffected()
}
//UpAb 更新AB实验
func (d *Dao) UpAb(c context.Context, id int, name, desc string, stra string, result int, username string, newVersion, status, group int) (rowsAffected int64, err error) {
var res xsql.Result
if res, err = d.db.Exec(c, _upAB, name, desc, stra, result, username, newVersion, id); err != nil {
return
}
return res.RowsAffected()
}
//UpStatus 更新AB实验状态
func (d *Dao) UpStatus(c context.Context, id, status int, username string, group int) (rowsAffected int64, err error) {
var res xsql.Result
if res, err = d.db.Exec(c, _upStatus, status, username, id); err != nil {
return
}
return res.RowsAffected()
}
//Incr stat result, save to redis
func (d *Dao) Incr(c context.Context, key string) {
conn := d.redis.Get(c)
defer conn.Close()
conn.Do("incr", key)
conn.Do("expire", key, 86400*7)
}
//Move backup key
func (d *Dao) Move(c context.Context, key string) {
conn := d.redis.Get(c)
defer conn.Close()
i, _ := redis.Int(conn.Do("get", key))
if _, err := conn.Do("setex", "O:"+key, 86400*7, i); err == nil {
conn.Do("del", key)
}
}
// GetFromRedis .
func (d *Dao) GetFromRedis(c context.Context, key string) (value int, err error) {
conn := d.redis.Get(c)
defer conn.Close()
value, err = redis.Int(conn.Do("get", key))
return
}

View File

@@ -0,0 +1,86 @@
package dao
import (
"context"
"flag"
"fmt"
"math/rand"
"testing"
"go-common/app/service/openplatform/abtest/conf"
"go-common/library/cache/redis"
. "github.com/smartystreets/goconvey/convey"
)
var testID int
func init() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(fmt.Errorf("conf.Init() error(%v)", err))
}
d = New(conf.Conf)
}
func TestActByGroup(t *testing.T) {
Convey("TestActByGroup: ", t, func() {
_, err := d.ActByGroup(context.TODO(), 1)
So(err, ShouldBeNil)
})
}
func TestListAb(t *testing.T) {
Convey("TestListAb: ", t, func() {
_, _, err := d.ListAb(context.TODO(), 1, 2, "0,1,2")
So(err, ShouldBeNil)
})
}
var testCase = "testxx"
func TestAddAb(t *testing.T) {
Convey("TestAddAb: ", t, func() {
var err error
testID, err = redis.Int(d.AddAb(context.TODO(), "test", "test", `{"precision":100,"ratio":[80,20]}`, rand.Intn(10000000), 0, 1, "test"))
So(err, ShouldBeNil)
So(testID, ShouldNotEqual, 0)
})
}
func TestUpAb(t *testing.T) {
Convey("TestUpAb: ", t, func() {
res, err := d.UpAb(context.TODO(), testID, "test", testCase, `{"precision":100,"ratio":[80,20]}`, 0, "update", 1, 0, 1)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}
func TestAb(t *testing.T) {
Convey("TestAb: ", t, func() {
res, err := d.Ab(context.TODO(), testID)
So(err, ShouldBeNil)
So(res.Desc, ShouldEqual, testCase)
})
}
func TestUpStatus(t *testing.T) {
Convey("TestUpSta: ", t, func() {
res, err := d.UpStatus(context.TODO(), testID, 1, "test", 1)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
Convey("TestUpSta: ", t, func() {
res, err := d.UpStatus(context.TODO(), testID, 0, "test", 1)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}
func TestDelStatus(t *testing.T) {
Convey("TestDelSta: ", t, func() {
res, err := d.DelAb(context.TODO(), testID)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}

View File

@@ -0,0 +1,57 @@
package dao
import (
"context"
"time"
"go-common/app/service/openplatform/abtest/conf"
"go-common/library/cache/redis"
"go-common/library/database/sql"
"go-common/library/log"
)
// Dao struct answer history of Dao
type Dao struct {
c *conf.Config
// db
db *sql.DB
// redis
redis *redis.Pool
expire int
verifyExpire int
}
// New new a Dao and return.
func New(c *conf.Config) (d *Dao) {
d = &Dao{
c: c,
db: sql.NewMySQL(c.DB.Ab),
redis: redis.NewPool(c.Redis.Config),
expire: int(time.Duration(c.Redis.Expire) / time.Second),
verifyExpire: int(time.Duration(c.Redis.VerifyCdTimes) / time.Second),
}
return
}
// Close close connections.
func (d *Dao) Close() {
if d.db != nil {
d.db.Close()
}
if d.redis != nil {
d.redis.Close()
}
}
// Ping ping health.
func (d *Dao) Ping(c context.Context) (err error) {
if err = d.db.Ping(c); err != nil {
log.Error("PingDb error(%v)", err)
return
}
if err = d.PingRedis(c); err != nil {
log.Error("PingRedis error(%v)", err)
return
}
return
}

View File

@@ -0,0 +1,17 @@
package dao
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
var d *Dao
func TestPing(t *testing.T) {
Convey("TestPing: ", t, func() {
err := d.Ping(context.TODO())
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,82 @@
package dao
import (
"context"
xsql "database/sql"
"fmt"
"go-common/app/service/openplatform/abtest/model"
"go-common/library/database/sql"
"github.com/pkg/errors"
)
const (
_addGroup = "INSERT INTO abtest_group (`name`,`desc`,`is_deleted`)VALUES(?, ?, 0)"
_listGroup = "SELECT `id`, `name`, `desc` FROM abtest_group WHERE `is_deleted` = 0 ORDER BY id"
_updateGroup = "UPDATE abtest_group set `name` = ?, `desc` = ? where id = ?"
_deleteGroup = "UPDATE abtest_group set `is_deleted` = 1 WHERE id = ?"
)
//AddGroup add a new group
func (d *Dao) AddGroup(c context.Context, g model.Group) (i int, err error) {
var (
res xsql.Result
)
if res, err = d.db.Exec(c, _addGroup, g.Name, g.Desc); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.group|AddGroup] d.db.Exec err: %v", err))
return
}
return intConv(res.LastInsertId())
}
//UpdateGroup update group by id
func (d *Dao) UpdateGroup(c context.Context, g model.Group) (i int, err error) {
var (
res xsql.Result
)
if res, err = d.db.Exec(c, _updateGroup, g.Name, g.Desc, g.ID); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.group|UpdateGroup] d.db.Exec err: %v", err))
return
}
return intConv(res.RowsAffected())
}
//DeleteGroup delete the group by id
func (d *Dao) DeleteGroup(c context.Context, id int) (i int, err error) {
var (
res xsql.Result
)
if res, err = d.db.Exec(c, _deleteGroup, id); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.group|DeleteGroup] d.db.Exec err: %v", err))
return
}
return intConv(res.RowsAffected())
}
//ListGroup list all groups
func (d *Dao) ListGroup(c context.Context) (res []*model.Group, err error) {
var rows *sql.Rows
if rows, err = d.db.Query(c, _listGroup); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.group|ListGroup] d.db.Query err: %v", err))
return
}
defer rows.Close()
for rows.Next() {
g := &model.Group{}
if err = rows.Scan(&g.ID, &g.Name, &g.Desc); err != nil {
err = errors.Wrap(err, fmt.Sprintf("[dao.group|ListGroup] d.db.Query err: %v", err))
return
}
res = append(res, g)
}
return
}
func intConv(i int64, err error) (int, error) {
if err != nil {
return 0, err
}
return int(i), nil
}

View File

@@ -0,0 +1,51 @@
package dao
import (
"context"
"testing"
"go-common/app/service/openplatform/abtest/model"
. "github.com/smartystreets/goconvey/convey"
)
var g = model.Group{
Name: "test",
Desc: "test add",
}
var testDesc = "test update"
func TestAddGroup(t *testing.T) {
Convey("TestAddGroup: ", t, func() {
var err error
testID, err = d.AddGroup(context.TODO(), g)
So(err, ShouldBeNil)
})
}
func TestUpdateGroup(t *testing.T) {
g.Desc = testDesc
g.ID = testID
Convey("TestUpdateGroup: ", t, func() {
res, err := d.UpdateGroup(context.TODO(), g)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}
func TestListGroup(t *testing.T) {
Convey("TestListGroup: ", t, func() {
res, err := d.ListGroup(context.TODO())
So(err, ShouldBeNil)
x := res[len(res)-1]
So(x.Name, ShouldEqual, g.Name)
})
}
func TestDeleteGroup(t *testing.T) {
Convey("TestDeleteGroup: ", t, func() {
res, err := d.DeleteGroup(context.TODO(), testID)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}

View File

@@ -0,0 +1,44 @@
package dao
import (
"context"
"fmt"
"go-common/library/cache/redis"
)
const (
_keyVersionID = "abtest:versionid:%d"
)
// PingRedis check redis connection
func (d *Dao) PingRedis(c context.Context) (err error) {
conn := d.redis.Get(c)
defer conn.Close()
_, err = conn.Do("PING")
return
}
//RedisVersionID 获取redis中的分组版本
func (d *Dao) RedisVersionID(c context.Context, group int) (ver int64, err error) {
conn := d.redis.Get(c)
defer conn.Close()
ver, err = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, group)))
return
}
//SetnxRedisVersionID 使用v设置redis中的版本号
func (d *Dao) SetnxRedisVersionID(c context.Context, group int, v int64) (err error) {
conn := d.redis.Get(c)
defer conn.Close()
_, err = conn.Do("SETNX", fmt.Sprintf(_keyVersionID, group), v)
return
}
//UpdateRedisVersionID 使用v更新redis中的分组版本
func (d *Dao) UpdateRedisVersionID(c context.Context, group int, v int64) (err error) {
conn := d.redis.Get(c)
defer conn.Close()
_, err = conn.Do("SETEX", fmt.Sprintf(_keyVersionID, group), d.verifyExpire, v)
return
}

View File

@@ -0,0 +1,99 @@
package dao
import (
"context"
"fmt"
"testing"
"go-common/library/cache/redis"
. "github.com/smartystreets/goconvey/convey"
)
func TestPingRedis(t *testing.T) {
Convey("TestPingRedis: ", t, func() {
err := d.PingRedis(context.TODO())
So(err, ShouldBeNil)
})
}
func TestGetRedisVersionID(t *testing.T) {
var (
ver int64
err error
)
ctx := context.TODO()
conn := d.redis.Get(ctx)
defer conn.Close()
Convey("TestGetRedisVersionID: ", t, func() {
if ver, _ = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1))); ver > 0 {
_, err = conn.Do("DEL", fmt.Sprintf(_keyVersionID, 1))
}
conn.Do("SET", fmt.Sprintf(_keyVersionID, 1), 123)
ver, err = d.RedisVersionID(ctx, 1)
So(ver, ShouldEqual, 123)
So(err, ShouldBeNil)
_, err = conn.Do("DEL", fmt.Sprintf(_keyVersionID, 1))
})
}
func TestSetnxRedisVersionID(t *testing.T) {
var (
ver int64
err error
)
ctx := context.TODO()
conn := d.redis.Get(ctx)
defer conn.Close()
Convey("TestSetnxRedisVersionID: ", t, func() {
if ver, _ = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1))); ver > 0 {
_, err = conn.Do("DEL", fmt.Sprintf(_keyVersionID, 1))
}
err = d.SetnxRedisVersionID(ctx, 1, 146)
So(err, ShouldBeNil)
ver, _ = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1)))
So(ver, ShouldEqual, 146)
_, err = conn.Do("DEL", fmt.Sprintf(_keyVersionID, 1))
})
}
func TestUpdateRedisVersionID(t *testing.T) {
var (
ver int64
err error
)
ctx := context.TODO()
conn := d.redis.Get(ctx)
defer conn.Close()
Convey("TestUpdateRedisVersionID: ", t, func() {
if ver, err = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1))); err == nil {
_, err = conn.Do("DEL", fmt.Sprintf(_keyVersionID, 1))
So(err, ShouldBeNil)
} else {
So(err, ShouldEqual, redis.ErrNil)
}
ver, err = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1)))
fmt.Println(ver)
So(err, ShouldEqual, redis.ErrNil)
err = d.UpdateRedisVersionID(ctx, 1, 123)
So(err, ShouldBeNil)
ver, err = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1)))
So(err, ShouldBeNil)
So(ver, ShouldEqual, 123)
err = d.UpdateRedisVersionID(ctx, 1, 132)
So(err, ShouldBeNil)
ver, err = redis.Int64(conn.Do("GET", fmt.Sprintf(_keyVersionID, 1)))
So(ver, ShouldEqual, 132)
So(err, ShouldBeNil)
_, err = conn.Do("DEL", fmt.Sprintf(_keyVersionID, 1))
So(err, ShouldBeNil)
})
}
func TestEnd(t *testing.T) {
d.Close()
}

View File

@@ -0,0 +1,60 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"group_test.go",
"http_test.go",
"stra_bench_test.go",
"stra_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/service:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"group.go",
"http.go",
"stra.go",
],
importpath = "go-common/app/service/openplatform/abtest/http",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/model:go_default_library",
"//app/service/openplatform/abtest/model/validator:go_default_library",
"//app/service/openplatform/abtest/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster: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,48 @@
package http
import (
"go-common/app/service/openplatform/abtest/model"
"go-common/app/service/openplatform/abtest/model/validator"
bm "go-common/library/net/http/blademaster"
)
//add group
func addGroup(c *bm.Context) {
params := new(validator.AddGroupParams)
if err := c.Bind(params); err != nil {
return
}
g := model.Group{
Name: params.Name,
Desc: params.Desc,
}
c.JSON(abSvr.AddGroup(c, g))
}
//list group
func listGroup(c *bm.Context) {
c.JSON(abSvr.ListGroup(c))
}
//update group
func updateGroup(c *bm.Context) {
params := new(validator.UpdateGroupParams)
if err := c.Bind(params); err != nil {
return
}
g := model.Group{
ID: params.ID,
Name: params.Name,
Desc: params.Desc,
}
c.JSON(abSvr.UpdateGroup(c, g))
}
//delete group
func deleteGroup(c *bm.Context) {
params := new(validator.DeleteGroupParams)
if err := c.Bind(params); err != nil {
return
}
c.JSON(abSvr.DeleteGroup(c, params.ID))
}

View File

@@ -0,0 +1,118 @@
package http
import (
"context"
"net/url"
"strconv"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
const (
_getGroupListURL = "http://localhost:8801/openplatform/admin/abtest/group/list"
_AddGroupURL = "http://localhost:8801/openplatform/admin/abtest/group/add"
_DelGroupURL = "http://localhost:8801/openplatform/admin/abtest/group/delete"
_UpdateGroupURL = "http://localhost:8801/openplatform/admin/abtest/group/update"
)
var agcs = []TestCase{
TestCase{tag: "TestAddGroup: valid parameters", testData: TestData{"name": "test", "desc": "test add"}, should: Shoulds{0}},
TestCase{tag: "TestAddGroup: empty parameters", testData: TestData{"name": "", "desc": ""}, should: Shoulds{-400}},
TestCase{tag: "TestAddGroup: no parameters", testData: TestData{}, should: Shoulds{-400}},
}
func TestAddGroup(t *testing.T) {
for _, td := range agcs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _AddGroupURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
Data int `json:"data"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
if res.Code == 0 {
testID = res.Data
}
})
}
}
func TestUpdateGroup(t *testing.T) {
var ugcs = []TestCase{
TestCase{tag: "TestUpdateGroup: valid parameters", testData: TestData{"id": strconv.Itoa(testID), "name": "test", "desc": "test update"}, should: Shoulds{0}},
TestCase{tag: "TestUpdateGroup: empty parameters", testData: TestData{"id": "0", "name": "", "desc": ""}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateGroup: no parameters", testData: TestData{}, should: Shoulds{-400}},
}
for _, td := range ugcs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _UpdateGroupURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
func TestDeleteGroup(t *testing.T) {
var dgcs = []TestCase{
TestCase{tag: "TestDeleteGroup: valid parameters", testData: TestData{"id": strconv.Itoa(testID), "name": "test", "desc": "test delete"}, should: Shoulds{0}},
TestCase{tag: "TestDeleteGroup: empty parameters", testData: TestData{"id": "0", "name": "", "desc": ""}, should: Shoulds{-400}},
TestCase{tag: "TestDeleteGroup: no parameters", testData: TestData{}, should: Shoulds{-400}},
}
for _, td := range dgcs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _DelGroupURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
func TestListGroup(t *testing.T) {
Convey("TestListGroup: ", t, func() {
params := url.Values{}
req, _ := client.NewRequest("GET", _getGroupListURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, 0)
})
}

View File

@@ -0,0 +1,80 @@
package http
import (
"net/http"
"go-common/app/service/openplatform/abtest/conf"
"go-common/app/service/openplatform/abtest/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
var (
abSvr *service.Service
)
// Init init account service.
func Init(c *conf.Config, s *service.Service) {
abSvr = s
engineInner := bm.DefaultServer(c.BM.Inner)
innerRouter(engineInner)
// init Inner serve
if err := engineInner.Start(); err != nil {
log.Error("bm.DefaultServer error(%v)", err)
panic(err)
}
engineOuter := bm.DefaultServer(c.BM.Outer)
outerRouter(engineOuter)
// init Outer serve
if err := engineOuter.Start(); err != nil {
log.Error("bm.DefaultServer error(%v)", err)
panic(err)
}
}
// innerRouter init inner router api path.
func innerRouter(e *bm.Engine) {
e.Ping(ping)
group := e.Group("/openplatform/internal/abtest")
{
//common
group.GET("/versionid", versionID)
group.GET("/version", version)
}
group = e.Group("/openplatform/admin/abtest")
{
//mng abtest
group.GET("/list", listAb)
group.GET("/add", addAb)
group.GET("/update", updateAb)
group.GET("/status", updateStatus)
group.GET("/delete", deleteAb)
}
group = e.Group("/openplatform/admin/abtest/group")
{
//mng group
group.GET("/add", addGroup)
group.GET("/list", listGroup)
group.GET("/update", updateGroup)
group.GET("/delete", deleteGroup)
}
group = e.Group("/openplatform/admin/abtest/stat")
{
group.GET("/total", total)
}
}
// outerRouter init outer router.
func outerRouter(e *bm.Engine) {
}
// ping check server ok.
func ping(c *bm.Context) {
if err := abSvr.Ping(c); err != nil {
log.Error("open-abtest http service ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}

View File

@@ -0,0 +1,24 @@
package http
import (
"flag"
"fmt"
"go-common/app/service/openplatform/abtest/conf"
"go-common/app/service/openplatform/abtest/service"
httpx "go-common/library/net/http/blademaster"
_ "github.com/smartystreets/goconvey/convey"
)
var client *httpx.Client
func init() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(fmt.Errorf("conf.Init() error(%v)", err))
}
svr := service.New(conf.Conf)
client = httpx.NewClient(conf.Conf.HTTPClient.Read)
Init(conf.Conf, svr)
}

View File

@@ -0,0 +1,197 @@
package http
import (
"encoding/json"
"math/rand"
"strconv"
"strings"
"time"
"go-common/app/service/openplatform/abtest/model"
"go-common/app/service/openplatform/abtest/model/validator"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
func versionID(c *bm.Context) {
params := new(validator.VerionParams)
if err := c.Bind(params); err != nil {
return
}
c.JSON(abSvr.VersionID(c, params.Group))
}
func version(c *bm.Context) {
var (
key = c.Request.Form.Get("key")
verStr = c.Request.Form.Get("version")
ver = &model.Version{}
groupStr = c.Request.Form.Get("group")
appKey = c.Request.Form.Get("appkey")
group int
err error
)
if verStr != "" {
if err = json.Unmarshal([]byte(verStr), ver); err != nil {
log.Warn("[http.stra|version] json.Unmarshal(%s) err: %v", verStr, err)
}
}
if groupStr != "" {
if group, err = strconv.Atoi(groupStr); err != nil {
log.Warn("[http.stra|version] strconv.Atoi(group:%s) err: %v", groupStr, err)
c.JSON(nil, ecode.RequestErr)
return
}
}
c.JSON(abSvr.Version(c, group, key, ver, appKey))
}
func listAb(c *bm.Context) {
var (
params = new(validator.ListParams)
data []*model.AB
total int
err error
)
if err = c.Bind(params); err != nil {
return
}
if err = checkStatus(params.Mstatus); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
if data, total, err = abSvr.ListAb(c, params.Pn, params.Ps, params.Mstatus, params.Group); err != nil {
log.Error("[http.stra|version] abSvr.ListAb(%d,%d) err: %v", params.Pn, params.Ps, err)
c.JSON(nil, err)
return
}
c.JSON(map[string]interface{}{
"result": data,
"total": total,
}, nil)
}
func addAb(c *bm.Context) {
var (
params = new(validator.AddAbParams)
ab = &model.AB{}
err error
)
if err = c.Bind(params); err != nil {
return
}
// params
if err = json.Unmarshal([]byte(params.Data), ab); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
if ab.Group == 0 {
ab.Group = params.Group
}
if ab.Group == 0 {
c.JSON(nil, ecode.RequestErr)
return
}
if params.Group != 0 && ab.Group != params.Group {
c.JSON(nil, ecode.RequestErr)
return
}
if !ab.Stra.Check() {
c.JSON(nil, ecode.RequestErr)
return
}
rand.Seed(time.Now().Unix())
c.JSON(abSvr.AddAb(c, ab))
}
func updateAb(c *bm.Context) {
var (
err error
ab *model.AB
srcAb model.AB
)
params := new(validator.UpdateAbParams)
if err = c.Bind(params); err != nil {
return
}
if ab, err = abSvr.Ab(c, params.ID, params.Group); err != nil {
c.JSON(nil, err)
return
}
if params.Group != 0 && ab.Group != params.Group {
c.JSON(nil, ecode.RequestErr)
return
}
srcAb = *ab
if err = json.Unmarshal([]byte(params.Data), ab); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
if !ab.Stra.Check() {
c.JSON(nil, ecode.RequestErr)
return
}
ab.Version = srcAb.Version
ab.Status = srcAb.Status
ab.Group = srcAb.Group
c.JSON(abSvr.UpdateAb(c, params.ID, ab))
}
func updateStatus(c *bm.Context) {
var (
ab *model.AB
err error
)
params := new(validator.UpdateStatusAbParams)
if err = c.Bind(params); err != nil {
return
}
if params.Status > 3 || params.Status < 0 {
c.JSON(nil, ecode.RequestErr)
return
}
if ab, err = abSvr.Ab(c, params.ID, params.Group); err != nil {
c.JSON(nil, err)
return
}
if params.Group != 0 && ab.Group != params.Group {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(abSvr.UpdateStatus(c, params.ID, params.Status, params.Modifier, ab.Group))
}
func deleteAb(c *bm.Context) {
var (
ab *model.AB
err error
)
params := new(validator.DelAbParams)
if err = c.Bind(params); err != nil {
return
}
if ab, err = abSvr.Ab(c, params.ID, params.Group); err != nil {
c.JSON(nil, err)
return
}
if params.Group != 0 && ab.Group != params.Group {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(abSvr.DeleteAb(c, params.ID))
}
func checkStatus(s string) (err error) {
strs := strings.Split(s, ",")
for _, a := range strs {
if _, err = strconv.Atoi(a); err != nil {
return
}
}
return
}
func total(c *bm.Context) {
c.JSON(abSvr.Total(c))
}

View File

@@ -0,0 +1,29 @@
package http
import (
"context"
"fmt"
"net/url"
"testing"
)
func BenchmarkVersion(b *testing.B) {
for i := 0; i < b.N; i++ {
params := url.Values{}
for k, v := range guscs[0].testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _getVersionURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
Data struct {
V int `json:"v"`
D interface{} `json:"d"`
} `json:"data"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
fmt.Println(err)
}
}
}

View File

@@ -0,0 +1,246 @@
package http
import (
"context"
"net/url"
"strconv"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
const (
_getListURL = "http://localhost:8801/openplatform/admin/abtest/list"
_getVersionIDURL = "http://localhost:8801/openplatform/internal/abtest/versionid"
_getVersionURL = "http://localhost:8801/openplatform/internal/abtest/version"
_AddURL = "http://localhost:8801/openplatform/admin/abtest/add"
_DelURL = "http://localhost:8801/openplatform/admin/abtest/delete"
_UpdateURL = "http://localhost:8801/openplatform/admin/abtest/update"
_UpdateStatusURL = "http://localhost:8801/openplatform/admin/abtest/status"
)
type TestData map[string]string
type Shoulds []interface{}
type TestCase struct {
tag string
testData TestData
should Shoulds
}
var gvcs = []TestCase{
TestCase{tag: "TestGetVersionID: valid parameters", testData: TestData{"group": "1"}, should: Shoulds{0}},
TestCase{tag: "TestGetVersionID: empty parameters", testData: TestData{"group": ""}, should: Shoulds{-400}},
TestCase{tag: "TestGetVersionID: invalid parameters", testData: TestData{"group": "asd"}, should: Shoulds{-400}},
TestCase{tag: "TestGetVersionID: no parameters", testData: TestData{}, should: Shoulds{-400}},
}
func TestGetVersionID(t *testing.T) {
for _, td := range gvcs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _getVersionIDURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
var guscs = []TestCase{
TestCase{tag: "TestGetVersion: valid parameters", testData: TestData{"group": "1", "key": "23232", "version": "{}"}, should: Shoulds{0}},
TestCase{tag: "TestGetVersion: no version", testData: TestData{"group": "1", "key": ""}, should: Shoulds{0}},
TestCase{tag: "TestGetVersion: no key", testData: TestData{"group": "1", "version": "{}"}, should: Shoulds{0}},
TestCase{tag: "TestGetVersion: no group", testData: TestData{"group": "1", "version": "{}"}, should: Shoulds{0}},
}
func TestGetVersion(t *testing.T) {
for _, td := range guscs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _getVersionURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
Data struct {
V int `json:"v"`
D interface{} `json:"d"`
} `json:"data"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
var glcs = []TestCase{
TestCase{tag: "TestGetListAb: valid parameters", testData: TestData{"pn": "1", "ps": "20", "mstatus": "1,2,0"}, should: Shoulds{0}},
TestCase{tag: "TestGetListAb: no pn", testData: TestData{"ps": "1", "mstatus": "1,2,0"}, should: Shoulds{-400}},
TestCase{tag: "TestGetListAb: no ps", testData: TestData{"pn": "1", "mstatus": "1,2,0"}, should: Shoulds{-400}},
TestCase{tag: "TestGetListAb: no mstatus", testData: TestData{"pn": "1", "ps": "10"}, should: Shoulds{-400}},
TestCase{tag: "TestGetListAb: invalid pn", testData: TestData{"pn": "a", "ps": "20", "mstatus": "1,2,0"}, should: Shoulds{-400}},
TestCase{tag: "TestGetListAb: invalid ps", testData: TestData{"pn": "1", "ps": "a", "mstatus": "1,2,0"}, should: Shoulds{-400}},
TestCase{tag: "TestGetListAb: invalid mstatus", testData: TestData{"pn": "1", "ps": "20", "mstatus": "a"}, should: Shoulds{-400}},
}
func TestGetListAb(t *testing.T) {
for _, td := range glcs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _getListURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
Data struct {
Result interface{} `json:"result"`
Total interface{} `json:"total"`
} `json:"data"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
var testID int
var adcs = []TestCase{
TestCase{tag: "TestAddAb: valid json", testData: TestData{"data": `{"name":"test1","desc":"test","stra":{"precision":100,"ratio":[20,80]},"result":1,"status":0,"group":1,"author":"test","modifer":"test"}`}, should: Shoulds{0}},
TestCase{tag: "TestAddAb: no permission", testData: TestData{"data": `{"name":"test1","desc":"test","stra":{"precision":100,"ratio":[20,80]},"result":1,"status":0,"group":1,"author":"test","modifer":"test"}`, "group": "2"}, should: Shoulds{-400}},
TestCase{tag: "TestAddAb: not json", testData: TestData{"data": `{"name":"test2","desc""test""stra":"{"precision":100,"ratio":[20,80]},"result":1,"status":0,"group":1,"author":"test","modifer":"test"}`}, should: Shoulds{-400}},
TestCase{tag: "TestAddAb: invalid stra", testData: TestData{"data": `{"name":"test3","desc":"test","stra":{"precision":100,"ratio":[20,70]},"result":1,"status":0,"group":1,"author":"test","modifer":"test"}`}, should: Shoulds{-400}},
TestCase{tag: "TestAddAb: no data", testData: TestData{}, should: Shoulds{-400}},
}
func TestAddAb(t *testing.T) {
for _, td := range adcs {
Convey(td.tag, t, func() {
var res struct {
Code int `json:"code"`
Data struct {
Newid int `json:"newid"`
} `json:"data"`
}
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _AddURL, "127.0.0.1", params)
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
if res.Code == 0 {
testID = res.Data.Newid
}
})
}
}
func TestUpdateAb(t *testing.T) {
var upcs = []TestCase{
TestCase{tag: "TestUpdateAb: valid params", testData: TestData{"id": strconv.Itoa(testID), "data": `{"name":"test","desc":"update","stra":{"precision":100,"ratio":[20,80]},"result":1,"status":0,"group":1,"author":"test","modifer":"test"}`}, should: Shoulds{0}},
TestCase{tag: "TestUpdateAb: no permission", testData: TestData{"id": strconv.Itoa(testID), "data": `{"name":"test1","desc":"test","stra":{"precision":100,"ratio":[20,80]},"result":1,"status":0,"group":2,"author":"test","modifer":"test"}`, "group": "2"}, should: Shoulds{-500}},
TestCase{tag: "TestUpdateAb: invalid params", testData: TestData{}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateAb: invalid id", testData: TestData{"id": "11111", "data": "aa"}, should: Shoulds{-500}},
TestCase{tag: "TestUpdateAb: invalid data", testData: TestData{"id": strconv.Itoa(testID), "data": "aa"}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateAb: valid stra", testData: TestData{"id": strconv.Itoa(testID), "data": `{"name":"test","desc":"update","stra":{"precision":100,"ratio":[20,81]},"result":1,"status":0,"group":1,"author":"test","modifer":"test"}`}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateAb: valid params group 0", testData: TestData{"id": strconv.Itoa(testID), "data": `{"name":"test","desc":"update2","stra":{"precision":100,"ratio":[20,80]},"result":1,"status":0,"group":0,"author":"test","modifer":"test"}`}, should: Shoulds{0}},
}
for _, td := range upcs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _UpdateURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
func TestUpdateStatusAb(t *testing.T) {
var upscs = []TestCase{
TestCase{tag: "TestUpdateStatusAb: valid params", testData: TestData{"id": strconv.Itoa(testID), "status": "1", "modifier": "test2"}, should: Shoulds{0}},
TestCase{tag: "TestUpdateStatusAb: no permission", testData: TestData{"id": strconv.Itoa(testID), "status": "1", "modifier": "test2", "group": "2"}, should: Shoulds{-500}},
TestCase{tag: "TestUpdateStatusAb: invalid params", testData: TestData{}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateStatusAb: invalid id", testData: TestData{"id": "11111", "data": "aa"}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateStatusAb: invalid status", testData: TestData{"id": strconv.Itoa(testID), "status": "4", "modifier": "test2"}, should: Shoulds{-400}},
TestCase{tag: "TestUpdateStatusAb: valid params", testData: TestData{"id": strconv.Itoa(testID), "status": "3", "modifier": "test2"}, should: Shoulds{0}},
}
for _, td := range upscs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _UpdateStatusURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}
func TestDelAb(t *testing.T) {
var dacs = []TestCase{
TestCase{tag: "TestDelAb: no permission", testData: TestData{"id": strconv.Itoa(testID), "group": "2"}, should: Shoulds{-500}},
TestCase{tag: "TestDelAb: valid id", testData: TestData{"id": strconv.Itoa(testID)}, should: Shoulds{0}},
TestCase{tag: "TestDelAb: invalid id", testData: TestData{"id": "x"}, should: Shoulds{-400}},
}
for _, td := range dacs {
Convey(td.tag, t, func() {
params := url.Values{}
for k, v := range td.testData {
params.Set(k, v)
}
req, _ := client.NewRequest("GET", _DelURL, "127.0.0.1", params)
var res struct {
Code int `json:"code"`
}
if err := client.Do(context.TODO(), req, &res); err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
So(res.Code, ShouldEqual, td.should[0])
})
}
}

View File

@@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["stra_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"abtest.go",
"group.go",
"stra.go",
"version.go",
],
importpath = "go-common/app/service/openplatform/abtest/model",
tags = ["automanaged"],
deps = [
"//library/log:go_default_library",
"//library/time:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/openplatform/abtest/model/jump:all-srcs",
"//app/service/openplatform/abtest/model/validator:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,31 @@
package model
import (
"go-common/library/time"
)
// AB AB测试实验
type AB struct {
ID int `json:"id"`
Name string `json:"name"`
Desc string `json:"desc"`
Stra Stra `json:"stra"`
Seed int `json:"seed"`
Result int `json:"result"`
Status int `json:"status"`
Version int `json:"version"`
Group int `json:"group"`
Author string `json:"author"`
Modifier string `json:"modifier"`
CreateTime time.Time `json:"ctime"`
ModifyTime time.Time `json:"mtime"`
}
// Stat .
type Stat struct {
New map[int]map[int]int `json:"now"`
Old map[int]map[int]int `json:"last"`
}
// Empty .
type Empty struct{}

View File

@@ -0,0 +1,8 @@
package model
//Group info of abtest
type Group struct {
ID int
Name string
Desc string
}

View File

@@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["jump_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["jump.go"],
importpath = "go-common/app/service/openplatform/abtest/model/jump",
tags = ["automanaged"],
)
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,30 @@
package jump
import (
"bytes"
"crypto/md5"
"encoding/binary"
)
//Hash get result by hash
func Hash(key uint64, numBuckets int) int32 {
var b int64 = -1
var j int64
for j < int64(numBuckets) {
b = j
key = key*2862933555777941757 + 1
j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
}
return int32(b)
}
//Md5 get result by Md5
func Md5(key string) uint64 {
var x uint64
s := md5.Sum([]byte(key))
b := bytes.NewBuffer(s[:])
binary.Read(b, binary.BigEndian, &x)
return x
}

View File

@@ -0,0 +1,40 @@
package jump
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
type hashStruct struct {
value uint64
exp int32
}
func Test_Hash(t *testing.T) {
Convey("Test_Hash: ", t, func() {
h := hashStruct{
value: uint64(314978625),
exp: int32(18),
}
bucket := 100
v := Hash(h.value, bucket)
So(v, ShouldEqual, h.exp)
})
}
type md5Struct struct {
value string
exp uint64
}
func Test_Md5(t *testing.T) {
Convey("Test_Hash: ", t, func() {
h := md5Struct{
value: "987654321",
exp: uint64(7979946199622949865),
}
v := Md5(h.value)
So(v, ShouldEqual, h.exp)
})
}

View File

@@ -0,0 +1,53 @@
package model
import (
"errors"
"go-common/library/log"
)
//Stra 实验策略
type Stra struct {
//精度
Precision int `json:"precision"`
//依次比例
Ratio []int `json:"ratio"`
}
func (s *Stra) check() (isValid bool) {
sum := 0
for _, r := range s.Ratio {
sum += r
}
isValid = (sum == s.Precision)
return
}
//Check ensure stra valid
func (s *Stra) Check() (isValid bool) {
return s.check()
}
//Version calculate version by score
func (s *Stra) Version(score int) (version int, err error) {
if !s.check() {
err = errors.New("the sum of ratio is not equal to precision")
log.Error("[model.stra|Version] s.check failed")
return
}
if score >= s.Precision || score < 0 {
err = errors.New("score should between 0 and s.Precision")
return
}
for i, r := range s.Ratio {
if score >= r {
score -= r
} else {
version = i
break
}
}
return
}

View File

@@ -0,0 +1,40 @@
package model
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
var stra = []Stra{
Stra{Precision: 100, Ratio: []int{10, 90}},
Stra{Precision: 100, Ratio: []int{10, 9}},
}
func TestCheck(t *testing.T) {
Convey("TestCheck: ", t, func() {
var checks = []bool{true, false}
for i, s := range stra {
got := s.Check()
So(got, ShouldEqual, checks[i])
}
})
}
func TestVersion(t *testing.T) {
testCase := map[int]int{9: 0, 20: 1}
s := stra[0]
Convey("TestVersion: ", t, func() {
for j, k := range testCase {
got, _ := s.Version(j)
So(got, ShouldEqual, k)
}
_, err := s.Version(101)
So(err, ShouldNotBeNil)
_, err = stra[1].Version(101)
So(err, ShouldNotBeNil)
})
}

View File

@@ -0,0 +1,30 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"group.go",
"stra.go",
],
importpath = "go-common/app/service/openplatform/abtest/model/validator",
tags = ["automanaged"],
)
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 validator
//AddGroupParams add group params validate
type AddGroupParams struct {
Name string `form:"name" validate:"required"`
Desc string `form:"desc" validate:"required"`
}
//UpdateGroupParams update group params validate
type UpdateGroupParams struct {
ID int `form:"id" validate:"required"`
Name string `form:"name" validate:"required"`
Desc string `form:"desc" validate:"required"`
}
//DeleteGroupParams delete group params validate
type DeleteGroupParams struct {
ID int `form:"id" validate:"required"`
}

View File

@@ -0,0 +1,41 @@
package validator
//VerionParams presents version params
type VerionParams struct {
Group int `form:"group" validate:"min=0,required"`
}
//ListParams presents listAb params
type ListParams struct {
Pn int `form:"pn" validate:"min=1,required"`
Ps int `form:"ps" validate:"min=1,required"`
Mstatus string `form:"mstatus" validate:"required"`
Group int `form:"group"`
}
//AddAbParams presents addAbtest params
type AddAbParams struct {
Data string `form:"data" validate:"required"`
Group int `form:"group"`
}
//UpdateAbParams presents updateAbtest params
type UpdateAbParams struct {
ID int `form:"id" validate:"required"`
Data string `form:"data" validate:"required"`
Group int `form:"group"`
}
//DelAbParams presents deleteAbtest params
type DelAbParams struct {
ID int `form:"id" validate:"required"`
Group int `form:"group"`
}
//UpdateStatusAbParams presents updateStatusAbtest params
type UpdateStatusAbParams struct {
ID int `form:"id" validate:"required"`
Status int `form:"status"`
Modifier string `form:"modifier" validate:"required"`
Group int `form:"group"`
}

View File

@@ -0,0 +1,7 @@
package model
// Version Cookie中保存的版本信息
type Version struct {
VersionID int64 `json:"v"`
Data map[int]int `json:"d"`
}

View File

@@ -0,0 +1,59 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"abtest_test.go",
"cache_test.go",
"group_test.go",
"service_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"abtest.go",
"cache.go",
"group.go",
"service.go",
],
importpath = "go-common/app/service/openplatform/abtest/service",
tags = ["automanaged"],
deps = [
"//app/service/openplatform/abtest/conf:go_default_library",
"//app/service/openplatform/abtest/dao:go_default_library",
"//app/service/openplatform/abtest/model:go_default_library",
"//app/service/openplatform/abtest/model/jump:go_default_library",
"//library/cache/redis:go_default_library",
"//library/log: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,250 @@
package service
import (
"context"
"encoding/json"
"math/rand"
"strconv"
"strings"
"time"
"go-common/app/service/openplatform/abtest/model"
"go-common/app/service/openplatform/abtest/model/jump"
"go-common/library/cache/redis"
"go-common/library/log"
)
//resetGroupCache 使用数据库信息重置分组缓存
func (s *Service) resetGroupCache(c context.Context, group int, ver int64) (err error) {
var src []*model.AB
if src, err = s.d.ActByGroup(c, group); err != nil {
return
}
err = s.setGroupCache(c, group, src, ver)
return
}
//groupCache 获取分组缓存
func (s *Service) groupCache(c context.Context, group int) (cache map[int]*model.AB, err error) {
var (
ok bool
ver int64
)
for cache, ok = s.readGroupCache(c, group); !ok; cache, ok = s.readGroupCache(c, group) {
if ver, err = s.d.RedisVersionID(c, group); err != nil {
if err == redis.ErrNil {
if err = s.d.SetnxRedisVersionID(c, group, time.Now().Unix()); err != nil {
log.Error("[service.abtest|groupCache] s.d.SetnxRedisVersionID err: %v", err)
return
}
} else {
log.Error("[service.abtest|groupCache] s.d.RedisVersionID err: %v", err)
return
}
}
s.resetGroupCache(c, group, ver)
}
return
}
func syncStart(s *Service) {
ctx := context.TODO()
for {
s.SyncVersionID(ctx)
time.Sleep(time.Second * 30)
}
}
//SyncVersionID 同步版本
func (s *Service) SyncVersionID(c context.Context) {
var (
err error
redisVer int64
cacheVerList map[int]int64
)
if cacheVerList, err = s.VersionIDListCache(c); err != nil {
log.Error("[service.abtest|SyncVersionID] VersionIDListCache err: %v", err)
return
}
for group, cacheVer := range cacheVerList {
if redisVer, err = s.d.RedisVersionID(c, group); err != nil {
if err == redis.ErrNil {
if err = s.d.SetnxRedisVersionID(c, group, time.Now().Unix()); err != nil {
log.Error("[service.abtest|groupCache] s.d.SetnxRedisVersionID err: %v", err)
return
}
} else {
log.Error("[service.abtest|groupCache] s.d.RedisVersionID err: %v", err)
return
}
}
if redisVer > cacheVer {
if err = s.resetGroupCache(c, group, redisVer); err != nil {
log.Error("[service.abtest|SyncVersionID] resetGroupCache(group:%d) err: %v", group, err)
continue
}
} else if redisVer < cacheVer {
s.d.UpdateRedisVersionID(c, group, cacheVer)
}
}
}
// versionByKey 根据key选择现行AB测试配置
func (s *Service) versionByKey(c context.Context, group int, key string) (abMap map[int]int, err error) {
var cache map[int]*model.AB
abMap = make(map[int]int)
if cache, err = s.groupCache(c, group); err != nil {
log.Error("[service.abtest|versionByKey] s.groupCache err: %v", err)
return
}
for _, ele := range cache {
if ele.Status != 1 || ele.Group != group {
continue
}
if key != "" {
//key不为空时散列后计算得到result
score := int(jump.Hash(jump.Md5(strings.Join([]string{key, string(ele.Seed)}, "")), ele.Stra.Precision))
if abMap[ele.ID], err = ele.Stra.Version(score); err != nil {
log.Error("[service.abtest|versionByKey] stra.Version err: %v", err)
return
}
} else {
abMap[ele.ID] = ele.Result
}
}
return
}
//VersionID returns current version ID
func (s *Service) VersionID(c context.Context, group int) (ver int64, err error) {
return s.versionID(c, group), nil
}
//Version 获取用户AB测试配置
func (s *Service) Version(c context.Context, group int, key string, ver *model.Version, appkey string) (res *model.Version, err error) {
res = ver
nVer := s.versionID(c, group)
if nVer > ver.VersionID {
res.VersionID = nVer
res.Data, err = s.versionByKey(c, group, key)
}
if s.c.Stat == 1 {
go s.stat(c, res, appkey)
// log.Info("key: %s, result: %s", key, res.Data[s.c.Stra])
}
return
}
//Ab 获取单个实验
func (s *Service) Ab(c context.Context, id int, group int) (res *model.AB, err error) {
if group == 0 {
return s.d.Ab(c, id)
}
return s.d.AbByIDAndGroup(c, id, group)
}
//ListAb 获取实验列表
func (s *Service) ListAb(c context.Context, page, pageSize int, mstatus string, group int) (res []*model.AB, total int, err error) {
offset := (page - 1) * pageSize
if group == 0 {
return s.d.ListAb(c, offset, pageSize, mstatus)
}
return s.d.ListAbByGroup(c, offset, pageSize, mstatus, group)
}
//AddAb 添加实验
func (s *Service) AddAb(c context.Context, ab *model.AB) (res map[string]interface{}, err error) {
var (
newID int64
seed int
stra []byte
)
seed = rand.Intn(1000000000)
if stra, err = json.Marshal(ab.Stra); err != nil {
return
}
if newID, err = s.d.AddAb(c, ab.Name, ab.Desc, string(stra), seed, ab.Result, ab.Group, ab.Author); err != nil {
return
}
res = map[string]interface{}{
"newid": newID,
}
return
}
//UpdateAb 更新实验
func (s *Service) UpdateAb(c context.Context, id int, ab *model.AB) (res bool, err error) {
var stra []byte
if stra, err = json.Marshal(ab.Stra); err != nil {
return
}
if _, err = s.d.UpAb(c, id, ab.Name, ab.Desc, string(stra), ab.Result, ab.Modifier, ab.Version+1, ab.Status, ab.Group); err != nil {
return
}
if err = s.resetGroupCache(c, ab.Group, time.Now().Unix()); err != nil {
return
}
res = true
go s.statU(c)
return
}
//UpdateStatus 更新实验状态
func (s *Service) UpdateStatus(c context.Context, id, status int, username string, group int) (res bool, err error) {
if _, err = s.d.UpStatus(c, id, status, username, group); err != nil {
return
}
if err = s.resetGroupCache(c, group, time.Now().Unix()); err != nil {
return
}
res = true
return
}
//DeleteAb 删除实验
func (s *Service) DeleteAb(c context.Context, id int) (res bool, err error) {
var row int64
if row, err = s.d.DelAb(c, id); err != nil {
return
}
if row != 0 {
res = true
}
return
}
func (s *Service) stat(c context.Context, data *model.Version, appkey string) {
for k, v := range data.Data {
key := "STAT:" + appkey + ":" + strconv.Itoa(k) + ":" + strconv.Itoa(v)
if _, ok := s.keyList.Load(key); !ok {
s.keyList.Store(key, model.Empty{})
}
s.d.Incr(c, key)
}
}
func (s *Service) statU(c context.Context) {
s.keyList.Range(func(key interface{}, value interface{}) bool {
s.d.Move(c, key.(string))
return true
})
}
//Total total of abtest result
func (s *Service) Total(c context.Context) (res map[string]map[string]int, err error) {
res0 := make(map[string]int)
res1 := make(map[string]int)
res = make(map[string]map[string]int)
s.keyList.Range(func(key interface{}, value interface{}) bool {
if v, err := s.d.GetFromRedis(c, key.(string)); err == nil {
res0[key.(string)] = v
}
if v, err := s.d.GetFromRedis(c, "O:"+key.(string)); err == nil {
res1[key.(string)] = v
}
res["current"] = res0
res["last"] = res1
return true
})
return
}

View File

@@ -0,0 +1,99 @@
package service
import (
"context"
"math/rand"
"testing"
"go-common/app/service/openplatform/abtest/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
testVer int64
testID int
testAb *model.AB
)
func TestSyncVersionID(t *testing.T) {
Convey("TestSyncVersionID: ", t, func() {
svr.SyncVersionID(context.TODO())
})
}
func TestVersionID(t *testing.T) {
var err error
Convey("TestVersionID: ", t, func() {
testVer, err = svr.VersionID(nil, 1)
So(err, ShouldBeNil)
})
}
func TestVersion(t *testing.T) {
Convey("TestVersion: ", t, func() {
over := &model.Version{}
_, err := svr.Version(context.TODO(), 1, "key", over, "1")
So(err, ShouldBeNil)
})
}
func TestListAb(t *testing.T) {
Convey("TestListAb: ", t, func() {
_, _, err := svr.ListAb(context.TODO(), 1, 10, "0,1,2", 1)
So(err, ShouldBeNil)
})
}
func TestAddAb(t *testing.T) {
var nab = &model.AB{
Name: "test",
Desc: "desc",
Stra: model.Stra{Precision: 100, Ratio: []int{80, 20}},
Seed: rand.Intn(1000000000),
Result: 0,
Group: 1,
Author: "test",
}
Convey("TestAddAb: ", t, func() {
res, err := svr.AddAb(context.TODO(), nab)
So(err, ShouldBeNil)
testID = int(res["newid"].(int64))
})
}
func TestAb(t *testing.T) {
var err error
Convey("TestAb: ", t, func() {
testAb, err = svr.Ab(context.TODO(), testID, 1)
So(err, ShouldBeNil)
})
}
func TestUpdateAb(t *testing.T) {
Convey("TestUpdateAb: ", t, func() {
testAb.Desc = "testUpdate"
res, err := svr.UpdateAb(context.TODO(), testID, testAb)
So(err, ShouldBeNil)
So(res, ShouldEqual, true)
})
}
func TestUpdateStatus(t *testing.T) {
Convey("TestUpdateStatus: ", t, func() {
res, err := svr.UpdateStatus(context.TODO(), testID, 1, "update", 1)
So(err, ShouldBeNil)
So(res, ShouldEqual, true)
svr.UpdateStatus(context.TODO(), testID, 0, "update", 1)
})
}
func TestDeleteAb(t *testing.T) {
Convey("TestDeleteAb: ", t, func() {
res, err := svr.DeleteAb(context.TODO(), testID)
So(err, ShouldBeNil)
So(res, ShouldEqual, true)
res, _ = svr.DeleteAb(context.TODO(), 1111)
So(res, ShouldEqual, false)
})
}

View File

@@ -0,0 +1,64 @@
package service
import (
"context"
"time"
"go-common/app/service/openplatform/abtest/model"
)
//setGroupCache 使用src重置分组缓存
func (s *Service) setGroupCache(c context.Context, group int, src []*model.AB, ver int64) (err error) {
m := make(map[int]*model.AB)
for _, ab := range src {
m[ab.ID] = ab
}
// s.mutex.Lock()
// s.abCache[group] = m
// s._versionID[group] = ver
// s.mutex.Unlock()
s.abCache.Store(group, m)
s._versionID.Store(group, ver)
return
}
//readGroupCache 获取分组缓存不存在则ok返回false
func (s *Service) readGroupCache(c context.Context, group int) (res map[int]*model.AB, ok bool) {
// s.mutex.RLock()
// res, ok = s.abCache[group]
// s.mutex.RUnlock()
var v interface{}
if v, ok = s.abCache.Load(group); ok {
res = v.(map[int]*model.AB)
}
return
}
//VersionIDListCache 获取缓存版本列表
func (s *Service) VersionIDListCache(c context.Context) (res map[int]int64, err error) {
res = make(map[int]int64)
// s.mutex.RLock()
// for k, v := range s._versionID {
// res[k] = v
// }
// s.mutex.RUnlock()
s._versionID.Range(func(key, value interface{}) bool {
res[key.(int)] = value.(int64)
return true
})
return
}
//versionID get A/B test version ID by group
func (s *Service) versionID(c context.Context, group int) (ver int64) {
v, ok := s._versionID.Load(group)
// s.mutex.RLock()
// ver, ok = s._versionID[group]
// s.mutex.RUnlock()
if ok {
ver = v.(int64)
return
}
ver = time.Now().Unix()
return
}

View File

@@ -0,0 +1,34 @@
package service
import (
"context"
"testing"
"go-common/app/service/openplatform/abtest/model"
. "github.com/smartystreets/goconvey/convey"
)
var ab = model.AB{ID: 1828, Group: 1, Status: 1, Name: "n@m3"}
func TestSetGroupCache(t *testing.T) {
var src = []*model.AB{&ab}
Convey("TestSetGroupCache: ", t, func() {
err := svr.setGroupCache(context.TODO(), 1, src, 123)
So(err, ShouldBeNil)
d, ok := svr.readGroupCache(context.TODO(), 1)
So(ok, ShouldBeTrue)
So(d[1828], ShouldEqual, &ab)
})
}
func TestVersionIDListCache(t *testing.T) {
var src = []*model.AB{&ab}
svr.setGroupCache(context.TODO(), 1, src, 123)
Convey("TestVersionIDListCache: ", t, func() {
verList, err := svr.VersionIDListCache(context.TODO())
So(err, ShouldBeNil)
_, ok := verList[1]
So(ok, ShouldBeTrue)
})
}

View File

@@ -0,0 +1,27 @@
package service
import (
"context"
"go-common/app/service/openplatform/abtest/model"
)
//AddGroup add a new group
func (s *Service) AddGroup(c context.Context, g model.Group) (id int, err error) {
return s.d.AddGroup(c, g)
}
//UpdateGroup update group by id
func (s *Service) UpdateGroup(c context.Context, g model.Group) (id int, err error) {
return s.d.UpdateGroup(c, g)
}
//ListGroup list all groups
func (s *Service) ListGroup(c context.Context) (m []*model.Group, err error) {
return s.d.ListGroup(c)
}
//DeleteGroup delete group by id
func (s *Service) DeleteGroup(c context.Context, id int) (r int, err error) {
return s.d.DeleteGroup(c, id)
}

View File

@@ -0,0 +1,51 @@
package service
import (
"context"
"testing"
"go-common/app/service/openplatform/abtest/model"
. "github.com/smartystreets/goconvey/convey"
)
var g = model.Group{
Name: "test",
Desc: "test add",
}
var testDesc = "test update"
func TestAddGroup(t *testing.T) {
Convey("TestAddGroup: ", t, func() {
var err error
testID, err = svr.AddGroup(context.TODO(), g)
So(err, ShouldBeNil)
})
}
func TestUpdateGroup(t *testing.T) {
g.Desc = testDesc
g.ID = testID
Convey("TestUpdateGroup: ", t, func() {
res, err := svr.UpdateGroup(context.TODO(), g)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}
func TestListGroup(t *testing.T) {
Convey("TestListGroup: ", t, func() {
res, err := svr.ListGroup(context.TODO())
So(err, ShouldBeNil)
x := res[len(res)-1]
So(x.Name, ShouldEqual, g.Name)
})
}
func TestDeleteGroup(t *testing.T) {
Convey("TestDeleteGroup: ", t, func() {
res, err := svr.DeleteGroup(context.TODO(), testID)
So(err, ShouldBeNil)
So(res, ShouldEqual, 1)
})
}

View File

@@ -0,0 +1,50 @@
package service
import (
"context"
"sync"
"go-common/app/service/openplatform/abtest/conf"
"go-common/app/service/openplatform/abtest/dao"
)
// Service struct of service.
type Service struct {
d *dao.Dao
// conf
c *conf.Config
// groupId => AB.id => AB
// abCache map[int]map[int]*model.AB
// _versionID map[int]int64
// mutex sync.RWMutex
abCache sync.Map
_versionID sync.Map
keyList sync.Map
// statCacheO map[int]map[int]int
}
// New create service instance and return.
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
d: dao.New(c),
}
// s.statCache = make(map[int]map[int]int)
// s.statCacheO = make(map[int]map[int]int)
go syncStart(s)
return
}
// Close dao.
func (s *Service) Close() {
s.d.Close()
}
// Ping check server ok.
func (s *Service) Ping(c context.Context) (err error) {
if err = s.d.Ping(c); err != nil {
return
}
return
}

View File

@@ -0,0 +1,35 @@
package service
import (
"context"
"flag"
"fmt"
"testing"
"go-common/app/service/openplatform/abtest/conf"
. "github.com/smartystreets/goconvey/convey"
)
var (
svr *Service
)
func init() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(fmt.Errorf("conf.Init() error(%v)", err))
}
svr = New(conf.Conf)
}
func TestPing(t *testing.T) {
Convey("TestPing: ", t, func() {
err := svr.Ping(context.TODO())
So(err, ShouldBeNil)
})
}
func TestEnd(t *testing.T) {
svr.Close()
}