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

View File

@@ -0,0 +1,71 @@
### 好友关系 Job
### V1.8.4
> 1.增加十万粉丝成就消息
> 2.开始正式发送粉丝成就消息
### V1.8.2
> 1.增加十万粉丝成就消息
### V1.8.1
> 1.修正粉丝成就消息
### V1.8.0
> 1.增加粉丝成就消息
### V1.6.3
> 1.去除 golang notify 写
### V1.6.2
> 1.迁移 xints
### V1.6.1
> 1.增加 sub 线程数
### V1.6.0
> 1.删除最近关注统计逻辑
### V1.5.0
> 1.迁移到 bm
### V1.4.2
> 1.严格关注提醒检查
> 2.添加测试
> 3.修正关注使用的 redis
### V1.4.1
> 1.添加关注提醒功能
> 2.remove statsd
### V1.4.0
> 1.添加b+ special 更新缓存
### V1.3.0
> 1.关注分组
### V1.2.0
> 1.迁移大仓库
### v1.1.4
> 1.加入go通知队列通知其他业务方。兼容性措施
> 2.升级最新版的go-common6.16及go-bussinesv2.22.2
### V1.1.3
> 1.无限重试加报警
### V1.1.2
> 1.fix
### V1.1.2
> 1.添加缓存更新失败报警
### v1.0.3
> 1.增加ctime
### v1.0.2
> 1.加入ping
### v1.0.1
> 1.接入新的配置中心

View File

@@ -0,0 +1,12 @@
# Owner
linmiao
zhoujiahui
# Author
lintanghui
niuqiang
zhoujiahui
# Reviewer
lintanghui
zhoujiahui

View File

@@ -0,0 +1,17 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- linmiao
- lintanghui
- niuqiang
- zhoujiahui
labels:
- job
- job/main/relation
- main
options:
no_parent_owners: true
reviewers:
- lintanghui
- niuqiang
- zhoujiahui

View File

@@ -0,0 +1,10 @@
#### relation-job
##### 项目简介
> 1.关系链的job异步清楚关注关系缓存
##### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
##### 依赖包
> 1.公共包go-common

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 = ["relation-job.toml"],
importpath = "go-common/app/job/main/relation/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/job/main/relation/conf:go_default_library",
"//app/job/main/relation/http:go_default_library",
"//app/job/main/relation/service: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,54 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"go-common/app/job/main/relation/conf"
"go-common/app/job/main/relation/http"
"go-common/app/job/main/relation/service"
"go-common/library/log"
)
var (
srv *service.Service
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
log.Init(conf.Conf.Xlog)
defer log.Close()
log.Info("go-common/app/job/main/relation start")
srv = service.New(conf.Conf)
http.Init(conf.Conf, srv)
signalHandler()
}
func signalHandler() {
var (
err error
ch = make(chan os.Signal, 1)
)
signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
si := <-ch
switch si {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
log.Info("get a signal %s, stop the consume process", si.String())
if err = srv.Close(); err != nil {
log.Error("srv close consumer error(%v)", err)
}
srv.Wait()
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,119 @@
# This is a TOML document. Boom.
version = "1.0.0"
user = "nobody"
dir = "./"
perf = "0.0.0.0:6330"
family = "relation-job"
env = "test"
[xlog]
dir = "/data/log/relation-job"
[multiHTTP]
[multiHTTP.inner]
addrs = ["0.0.0.0:6331"]
maxListen = 1000
[mysql]
addr = "172.16.33.101:3306"
dsn = "account:wx2U1MwXRyWEuURw@tcp(172.16.33.101:3306)/relation?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 5
idle = 2
idleTimeout ="4h"
queryTimeout = "100ms"
execTimeout = "100ms"
tranTimeout = "200ms"
[mysql.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[memcache]
name = "relation-job"
proto = "tcp"
addr = "192.168.99.100:32777"
idle = 5
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "24h"
[redis]
name = "go-common/app/job/main/relation"
proto = "tcp"
addr = "127.0.0.1:32769"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "720h"
[relRedis]
name = "relation-service"
proto = "tcp"
addr = "127.0.0.1:32769"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "720h"
[clearPath]
following = "http://api.bilibili.co/x/internal/relation/cache/following/update"
follower = "http://api.bilibili.co/x/internal/relation/cache/follower/del"
stat = "http://api.bilibili.co/x/internal/relation/cache/stat/del"
[apiPath]
followersNotify = "http://message.bilibili.co/api/notify/send.user.notify.do"
[httpClient]
key = "e7482d29be4a95b8"
secret = "9e803791cdef756e75faee68e12b7442"
dial = "100ms"
timeout = "1s"
keepAlive = "60s"
timer = 128
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[app]
key = "e7482d29be4a95b8"
secret = "9e803791cdef756e75faee68e12b7442"
[databus]
key = "0QBAaL5f8hupBjLQydZt"
secret = "0QBAaL5f8hupBjLQydZu"
group = "Relation-Friends-S"
topic = "Relation-T"
action = "sub"
offset = "old"
buffer = 2048
name = "go-common/app/job/main/relation/databus"
proto = "tcp"
addr = "172.16.33.158:6205"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "60s"
writeTimeout = "1s"
idleTimeout = "10s"
[sms]
phone="15121003430,13601914616"
token="460d52f1-dafe-44a2-911a-9f308968309a"
[relation]
followersUnread = "1m"

View File

@@ -0,0 +1,39 @@
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/job/main/relation/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/memcache:go_default_library",
"//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/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,141 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/memcache"
"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/queue/databus"
"go-common/library/time"
"github.com/BurntSushi/toml"
)
// Conf global variable.
var (
confPath string
Conf = &Config{}
client *conf.Client
)
// Config struct of conf.
type Config struct {
// base
// app
App *bm.App
// Env
Env string
// goroutine sleep
Tick time.Duration
// log
Xlog *log.Config
// db
Mysql *sql.Config
// databus
DataBus *databus.Config
// httpClinet
HTTPClient *bm.ClientConfig
// clearPath
ClearPath *ClearPath
// apiPath
ApiPath *ApiPath
// sms monitor
Sms *Sms
// redis
// Redis *Redis
RelRedis *Redis
Memcache *Memcache
Relation *Relation
// bm
BM *bm.ServerConfig
}
// ClearPath clear cache path
type ClearPath struct {
Following string
Follower string
Stat string
}
// ApiPath api path collections
type ApiPath struct {
FollowersNotify string
}
// Sms is sms monitor config.
type Sms struct {
Phone string
Token string
}
type Redis struct {
*redis.Config
Expire time.Duration
}
type Memcache struct {
*memcache.Config
Expire time.Duration
FollowerExpire time.Duration
}
// Relation relation related config
type Relation struct {
// followers unread duration
// FollowersUnread time.Duration
}
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
func Init() (err 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,70 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"cache_update_test.go",
"dao_test.go",
"follower_test.go",
"following_test.go",
"mysql_test.go",
"stat_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/job/main/relation/conf:go_default_library",
"//app/job/main/relation/model:go_default_library",
"//app/service/main/relation/model:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"cache_update.go",
"dao.go",
"follower.go",
"following.go",
"mysql.go",
"stat.go",
],
importpath = "go-common/app/job/main/relation/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/job/main/relation/conf:go_default_library",
"//app/job/main/relation/model:go_default_library",
"//app/job/main/relation/model/i64b:go_default_library",
"//app/service/main/relation/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster: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"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,135 @@
package dao
import (
"context"
"fmt"
"strconv"
"go-common/app/service/main/relation/model"
gmc "go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/log"
"go-common/library/time"
)
const (
_prefixFollowings = "at_"
_prefixTags = "tags_" // user tag info.
)
func tagsKey(mid int64) string {
return _prefixTags + strconv.FormatInt(mid, 10)
}
func followingsKey(mid int64) string {
return _prefixFollowings + strconv.FormatInt(mid, 10)
}
// ==== redis ===
// AddFollowingCache add following cache.
func (d *Dao) AddFollowingCache(c context.Context, mid int64, following *model.Following) (err error) {
var (
ok bool
key = followingsKey(mid)
)
conn := d.relRedis.Get(c)
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.relExpire)); err != nil {
log.Error("redis.Bool(conn.Do(EXPIRE, %s)) error(%v)", key, err)
} else if ok {
var ef []byte
if ef, err = d.encode(following.Attribute, following.MTime, following.Tag, following.Special); err != nil {
return
}
if _, err = conn.Do("HSET", key, following.Mid, ef); err != nil {
log.Error("conn.Do(HSET, %s, %d) error(%v)", key, following.Mid, err)
}
}
conn.Close()
return
}
// DelFollowing del following cache.
func (d *Dao) DelFollowing(c context.Context, mid int64, following *model.Following) (err error) {
var (
ok bool
key = followingsKey(mid)
)
conn := d.relRedis.Get(c)
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.relExpire)); err != nil {
log.Error("redis.Bool(conn.Do(EXPIRE, %s)) error(%v)", key, err)
} else if ok {
if _, err = conn.Do("HDEL", key, following.Mid); err != nil {
log.Error("conn.Do(HDEL, %s, %d) error(%v)", key, following.Mid, err)
}
}
conn.Close()
return
}
// encode
func (d *Dao) encode(attribute uint32, mtime time.Time, tagids []int64, special int32) (res []byte, err error) {
ft := &model.FollowingTags{Attr: attribute, Ts: mtime, TagIds: tagids, Special: special}
return ft.Marshal()
}
// ===== memcache =====
const (
_prefixFollowing = "pb_a_"
_prefixTagCount = "rs_tmtc_%d" // key of relation tag by mid & tag's count
)
func followingKey(mid int64) string {
return _prefixFollowing + strconv.FormatInt(mid, 10)
}
func tagCountKey(mid int64) string {
return fmt.Sprintf(_prefixTagCount, mid)
}
// DelFollowingCache delete following cache.
func (d *Dao) DelFollowingCache(c context.Context, mid int64) (err error) {
return d.delFollowingCache(c, followingKey(mid))
}
// delFollowingCache delete following cache.
func (d *Dao) delFollowingCache(c context.Context, key string) (err error) {
conn := d.mc.Get(c)
if err = conn.Delete(key); err != nil {
if err == gmc.ErrNotFound {
err = nil
} else {
log.Error("conn.Delete(%s) error(%v)", key, err)
}
}
conn.Close()
return
}
// DelTagCountCache del tag count cache.
func (d *Dao) DelTagCountCache(c context.Context, mid int64) (err error) {
conn := d.mc.Get(c)
if err = conn.Delete(tagCountKey(mid)); err != nil {
if err == gmc.ErrNotFound {
err = nil
} else {
log.Error("conn.Delete(%s) error(%v)", tagCountKey(mid), err)
}
}
conn.Close()
return
}
// DelTagsCache is
func (d *Dao) DelTagsCache(c context.Context, mid int64) (err error) {
conn := d.mc.Get(c)
if err = conn.Delete(tagsKey(mid)); err != nil {
if err == gmc.ErrNotFound {
err = nil
} else {
log.Error("conn.Delete(%s) error(%v)", tagCountKey(mid), err)
}
}
conn.Close()
return
}

View File

@@ -0,0 +1,156 @@
package dao
import (
"context"
"go-common/app/service/main/relation/model"
xtime "go-common/library/time"
"time"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaotagsKey(t *testing.T) {
var (
mid = int64(0)
)
convey.Convey("tagsKey", t, func(ctx convey.C) {
p1 := tagsKey(mid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaofollowingsKey(t *testing.T) {
var (
mid = int64(0)
)
convey.Convey("followingsKey", t, func(ctx convey.C) {
p1 := followingsKey(mid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaoAddFollowingCache(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
following = &model.Following{}
)
convey.Convey("AddFollowingCache", t, func(ctx convey.C) {
err := d.AddFollowingCache(c, mid, following)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelFollowing(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
following = &model.Following{}
)
convey.Convey("DelFollowing", t, func(ctx convey.C) {
err := d.DelFollowing(c, mid, following)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoencode(t *testing.T) {
var (
attribute = uint32(0)
mtime = xtime.Time(time.Now().Unix())
tagids = []int64{}
special = int32(0)
)
convey.Convey("encode", t, func(ctx convey.C) {
res, err := d.encode(attribute, mtime, tagids, special)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaofollowingKey(t *testing.T) {
var (
mid = int64(0)
)
convey.Convey("followingKey", t, func(ctx convey.C) {
p1 := followingKey(mid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaotagCountKey(t *testing.T) {
var (
mid = int64(0)
)
convey.Convey("tagCountKey", t, func(ctx convey.C) {
p1 := tagCountKey(mid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaoDelFollowingCache(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
)
convey.Convey("DelFollowingCache", t, func(ctx convey.C) {
err := d.DelFollowingCache(c, mid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaodelFollowingCache(t *testing.T) {
var (
c = context.Background()
key = ""
)
convey.Convey("delFollowingCache", t, func(ctx convey.C) {
err := d.delFollowingCache(c, key)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelTagCountCache(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
)
convey.Convey("DelTagCountCache", t, func(ctx convey.C) {
err := d.DelTagCountCache(c, mid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelTagsCache(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
)
convey.Convey("DelTagsCache", t, func(ctx convey.C) {
err := d.DelTagsCache(c, mid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,59 @@
package dao
import (
"context"
"time"
"go-common/app/job/main/relation/conf"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
xsql "go-common/library/database/sql"
bm "go-common/library/net/http/blademaster"
)
// Dao struct info of Dao.
type Dao struct {
c *conf.Config
client *bm.Client
//path
clearFollowingPath string
clearFollowerPath string
clearStatPath string
// api path
followersNotify string
// db
db *xsql.DB
// redis
// redis *redis.Pool
// redisExpire int32
// relation cache
relRedis *redis.Pool
relExpire int32
mc *memcache.Pool
// UnreadDuration int64
}
// New new a Dao and return.
func New(c *conf.Config) (dao *Dao) {
dao = &Dao{
c: c,
client: bm.NewClient(c.HTTPClient),
clearFollowingPath: c.ClearPath.Following,
clearFollowerPath: c.ClearPath.Follower,
clearStatPath: c.ClearPath.Stat,
followersNotify: c.ApiPath.FollowersNotify,
db: xsql.NewMySQL(c.Mysql),
// redis: redis.NewPool(c.Redis.Config),
// redisExpire: int32(time.Duration(c.RelRedis.Expire) / time.Second),
relRedis: redis.NewPool(c.RelRedis.Config),
relExpire: int32(time.Duration(c.RelRedis.Expire) / time.Second),
mc: memcache.NewPool(c.Memcache.Config),
// UnreadDuration: int64(time.Duration(c.Relation.FollowersUnread) / time.Second),
}
return dao
}
// Ping ping health of db.
func (d *Dao) Ping(c context.Context) (err error) {
return d.db.Ping(c)
}

View File

@@ -0,0 +1,33 @@
package dao
import (
"flag"
"go-common/app/job/main/relation/conf"
"os"
"testing"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.account.relation-job")
flag.Set("conf_token", "3c29797a4f1c9939b24d36cdc62a3a2b")
flag.Set("tree_id", "2138")
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")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
m.Run()
os.Exit(0)
}

View File

@@ -0,0 +1,32 @@
package dao
import (
"context"
"net/url"
"strconv"
"go-common/library/log"
)
// consts
const (
AttrFollowing = uint32(1) << 1
AttrFriend = uint32(1) << 2
)
// DelFollowerCache del follower cache
func (d *Dao) DelFollowerCache(fid int64) (err error) {
params := url.Values{}
params.Set("mid", strconv.FormatInt(fid, 10))
var res struct {
Code int `json:"code"`
}
if err = d.client.Post(context.TODO(), d.clearFollowerPath, "", params, &res); err != nil {
log.Error("d.client.Post error(%v)", err)
return
}
if res.Code != 0 {
log.Error("url(%s) res code(%d) or res.result(%v)", d.clearFollowerPath+"?"+params.Encode(), res.Code)
}
return
}

View File

@@ -0,0 +1,19 @@
package dao
import (
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoDelFollowerCache(t *testing.T) {
var (
fid = int64(0)
)
convey.Convey("DelFollowerCache", t, func(ctx convey.C) {
err := d.DelFollowerCache(fid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,35 @@
package dao
import (
"context"
"net/url"
"strconv"
"time"
"go-common/app/job/main/relation/model"
"go-common/library/log"
)
// UpdateFollowingCache update following cache.
func (d *Dao) UpdateFollowingCache(r *model.Relation) (err error) {
params := url.Values{}
params.Set("mid", strconv.FormatInt(r.Mid, 10))
params.Set("fid", strconv.FormatInt(r.Fid, 10))
params.Set("attribute", strconv.FormatInt(int64(r.Attribute), 10))
mt, err := time.Parse(time.RFC3339, r.MTime)
if err != nil {
mt = time.Now()
}
params.Set("mtime", strconv.FormatInt(mt.Unix(), 10))
var res struct {
Code int `json:"code"`
}
if err = d.client.Post(context.TODO(), d.clearFollowingPath, "", params, &res); err != nil {
log.Error("d.client.Post error(%v)", err)
return
}
if res.Code != 0 {
log.Error("url(%s) res code(%d) or res.result(%v)", d.clearFollowingPath+"?"+params.Encode(), res.Code)
}
return
}

View File

@@ -0,0 +1,20 @@
package dao
import (
"go-common/app/job/main/relation/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoUpdateFollowingCache(t *testing.T) {
var (
r = &model.Relation{}
)
convey.Convey("UpdateFollowingCache", t, func(ctx convey.C) {
err := d.UpdateFollowingCache(r)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,57 @@
package dao
import (
"context"
"fmt"
"go-common/app/job/main/relation/model/i64b"
sml "go-common/app/service/main/relation/model"
"go-common/library/database/sql"
)
const (
_shard = 500
_tagUserShard = 500
// following
_getRelationSQL = "SELECT r.attribute,r.mtime,t.tag FROM user_relation_mid_%03d AS r join user_relation_tag_user_%03d AS t ON t.mid=r.mid AND t.fid=r.fid WHERE r.mid=? AND r.fid=? AND r.status=0 "
_UserSetAchieveFlag = "INSERT INTO user_addit (mid,achieve_flags) VALUES (?,?) ON DUPLICATE KEY UPDATE achieve_flags=achieve_flags|VALUES(achieve_flags)"
)
func hit(id int64) int64 {
return id % _shard
}
func tagUserHit(id int64) int64 {
return id % _tagUserShard
}
// UserRelation get user relation attr.
func (d *Dao) UserRelation(c context.Context, mid, fid int64) (f *sml.Following, err error) {
row := d.db.QueryRow(c, fmt.Sprintf(_getRelationSQL, hit(mid), tagUserHit(mid)), mid, fid)
f = new(sml.Following)
var ttag i64b.Int64Bytes
if err = row.Scan(&f.Attribute, &f.MTime, &ttag); err != nil {
if err == sql.ErrNoRows {
err = nil
f = nil
}
return
}
f.Mid = fid
f.Tag = []int64(ttag)
for _, id := range f.Tag {
if id == -10 {
f.Special = 1
}
}
return
}
// UserSetAchieveFlag is
func (d *Dao) UserSetAchieveFlag(ctx context.Context, mid int64, flag uint64) (int64, error) {
res, err := d.db.Exec(ctx, _UserSetAchieveFlag, mid, flag)
if err != nil {
return 0, err
}
return res.RowsAffected()
}

View File

@@ -0,0 +1,62 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaohit(t *testing.T) {
var (
id = int64(0)
)
convey.Convey("hit", t, func(ctx convey.C) {
p1 := hit(id)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaotagUserHit(t *testing.T) {
var (
id = int64(0)
)
convey.Convey("tagUserHit", t, func(ctx convey.C) {
p1 := tagUserHit(id)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaoUserRelation(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
fid = int64(0)
)
convey.Convey("UserRelation", t, func(ctx convey.C) {
f, err := d.UserRelation(c, mid, fid)
ctx.Convey("Then err should be nil.f should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(f, convey.ShouldNotBeNil)
})
})
}
func TestDaoUserSetAchieveFlag(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
flag = uint64(0)
)
convey.Convey("UserSetAchieveFlag", t, func(ctx convey.C) {
p1, err := d.UserSetAchieveFlag(c, mid, flag)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}

View File

@@ -0,0 +1,111 @@
package dao
import (
"context"
"fmt"
"math"
"net/url"
"strconv"
"go-common/app/job/main/relation/model"
"go-common/library/log"
)
// DelStatCache is
func (d *Dao) DelStatCache(mid int64) (err error) {
params := url.Values{}
params.Set("mid", strconv.FormatInt(mid, 10))
var res struct {
Code int `json:"code"`
}
if err = d.client.Post(context.TODO(), d.clearStatPath, "", params, &res); err != nil {
log.Error("d.client.Post error(%v)", err)
return
}
if res.Code != 0 {
log.Error("url(%s) res code(%d) or res.result(%v)", d.clearStatPath+"?"+params.Encode(), res.Code)
}
return
}
// FollowerAchieve is
func (d *Dao) FollowerAchieve(c context.Context, mid, follower int64) {
// 不为 0 结尾的就不检查了
if follower%10 != 0 {
return
}
flag := model.AchieveFromFollower(follower)
if flag <= 0 {
log.Warn("No achieve flag achieved with mid: %d, follower: %d", mid, follower)
return
}
effected, err := d.UserSetAchieveFlag(c, mid, uint64(flag))
if err != nil {
log.Error("Failed to set user achieve flag: mid: %d, flag: %d: %+v", mid, flag, err)
return
}
if effected <= 0 {
log.Info("Already achieved with mid: %d, flag: %d", mid, flag)
return
}
msg := func() string {
switch flag {
case model.FollowerAchieve1k:
return `恭喜您您的粉丝已经达到1000粉`
case model.FollowerAchieve5k:
return `恭喜您您的粉丝已经达到5000粉`
case model.FollowerAchieve10k:
return `恭喜您您的粉丝已经达到1万粉您将有机会获得UP主粉丝成就奖“一万粉丝成就奖励” #{戳我领取吧!}{"https://www.bilibili.com/blackboard/activity-zxIQ8otdK.html#/"}`
case model.FollowerAchieve100k:
return `恭喜您您的粉丝已经达到10万粉您将有机会获得UP主粉丝成就奖“十万粉丝成就奖励” #{戳我领取吧!}{"https://www.bilibili.com/blackboard/activity-zxIQ8otdK.html#/"}`
case model.FollowerAchieve1000k:
return `恭喜您您的粉丝已经达到100万粉您将有机会获得UP主粉丝成就奖“百万粉丝成就奖励” #{戳我领取吧!}{"https://www.bilibili.com/blackboard/activity-zxIQ8otdK.html#/"}`
}
if flag >= model.FollowerAchieve100k {
return fmt.Sprintf(`恭喜您,您的粉丝已达%d万粉`, int64((math.Log2(float64(flag))-2)*100000/10000))
}
return ""
}()
if msg != "" {
log.Info("Follower achieve send message to mid: %d: %s", mid, msg)
d.SendMsg(c, mid, "粉丝增长通知", msg)
}
d.ensureAllFollowerAchieve(c, mid, follower)
}
func (d *Dao) ensureAllFollowerAchieve(c context.Context, mid int64, follower int64) {
flags := model.AllAchieveFromFollower(follower)
v := model.AchieveFlag(0)
for _, f := range flags {
v |= f
}
effected, err := d.UserSetAchieveFlag(c, mid, uint64(v))
if err != nil {
log.Error("Failed to ensure user achieve flag: mid: %d, flags: %+v, follower: %d: %+v", mid, flags, follower, err)
return
}
if effected >= 0 {
log.Warn("Achieve missed on mid: %d, follower: %d, flags: %+v", mid, follower, flags)
return
}
}
// SendMsg send message.
func (d *Dao) SendMsg(c context.Context, mid int64, title string, context string) (err error) {
params := url.Values{}
params.Set("mc", "2_5_1")
params.Set("title", title)
params.Set("data_type", "4")
params.Set("context", context)
params.Set("mid_list", fmt.Sprintf("%d", mid))
var res struct {
Code int `json:"code"`
}
if err = d.client.Post(c, d.followersNotify, "", params, &res); err != nil || res.Code != 0 {
log.Error("sendMsgURL(%s) code(%d) error(%v)", d.followersNotify+"?"+params.Encode(), res.Code, err)
return
}
log.Info("d.sendMsgURL url(%s) res(%d)", d.followersNotify+"?"+params.Encode(), res.Code)
return
}

View File

@@ -0,0 +1,61 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoDelStatCache(t *testing.T) {
var (
mid = int64(0)
)
convey.Convey("DelStatCache", t, func(ctx convey.C) {
err := d.DelStatCache(mid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoFollowerAchieve(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
follower = int64(0)
)
convey.Convey("FollowerAchieve", t, func(ctx convey.C) {
d.FollowerAchieve(c, mid, follower)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
}
func TestDaoensureAllFollowerAchieve(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
follower = int64(0)
)
convey.Convey("ensureAllFollowerAchieve", t, func(ctx convey.C) {
d.ensureAllFollowerAchieve(c, mid, follower)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
}
func TestDaoSendMsg(t *testing.T) {
var (
c = context.Background()
mid = int64(0)
title = ""
context = ""
)
convey.Convey("SendMsg", t, func(ctx convey.C) {
err := d.SendMsg(c, mid, title, context)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,48 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"errors_test.go",
"fsm_test.go",
"relation_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"errors.go",
"event.go",
"fsm.go",
"realtion.go",
"utils.go",
],
importpath = "go-common/app/job/main/relation/fsm",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
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,100 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
// InvalidEventError is returned by FSM.Event() when the event cannot be called
// in the current state.
type InvalidEventError struct {
Event string
State string
}
func (e InvalidEventError) Error() string {
return "event " + e.Event + " inappropriate in current state " + e.State
}
// UnknownEventError is returned by FSM.Event() when the event is not defined.
type UnknownEventError struct {
Event string
}
func (e UnknownEventError) Error() string {
return "event " + e.Event + " does not exist"
}
// InTransitionError is returned by FSM.Event() when an asynchronous transition
// is already in progress.
type InTransitionError struct {
Event string
}
func (e InTransitionError) Error() string {
return "event " + e.Event + " inappropriate because previous transition did not complete"
}
// NotInTransitionError is returned by FSM.Transition() when an asynchronous
// transition is not in progress.
type NotInTransitionError struct{}
func (e NotInTransitionError) Error() string {
return "transition inappropriate because no state change in progress"
}
// NoTransitionError is returned by FSM.Event() when no transition have happened,
// for example if the source and destination states are the same.
type NoTransitionError struct {
Err error
}
func (e NoTransitionError) Error() string {
if e.Err != nil {
return "no transition with error: " + e.Err.Error()
}
return "no transition"
}
// CanceledError is returned by FSM.Event() when a callback have canceled a
// transition.
type CanceledError struct {
Err error
}
func (e CanceledError) Error() string {
if e.Err != nil {
return "transition canceled with error: " + e.Err.Error()
}
return "transition canceled"
}
// AsyncError is returned by FSM.Event() when a callback have initiated an
// asynchronous state transition.
type AsyncError struct {
Err error
}
func (e AsyncError) Error() string {
if e.Err != nil {
return "async started with error: " + e.Err.Error()
}
return "async started"
}
// InternalError is returned by FSM.Event() and should never occur. It is a
// probably because of a bug.
type InternalError struct{}
func (e InternalError) Error() string {
return "internal error on state transition"
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
import (
"errors"
"testing"
)
func TestInvalidEventError(t *testing.T) {
event := "invalid event"
state := "state"
e := InvalidEventError{Event: event, State: state}
if e.Error() != "event "+e.Event+" inappropriate in current state "+e.State {
t.Error("InvalidEventError string mismatch")
}
}
func TestUnknownEventError(t *testing.T) {
event := "invalid event"
e := UnknownEventError{Event: event}
if e.Error() != "event "+e.Event+" does not exist" {
t.Error("UnknownEventError string mismatch")
}
}
func TestInTransitionError(t *testing.T) {
event := "in transition"
e := InTransitionError{Event: event}
if e.Error() != "event "+e.Event+" inappropriate because previous transition did not complete" {
t.Error("InTransitionError string mismatch")
}
}
func TestNotInTransitionError(t *testing.T) {
e := NotInTransitionError{}
if e.Error() != "transition inappropriate because no state change in progress" {
t.Error("NotInTransitionError string mismatch")
}
}
func TestNoTransitionError(t *testing.T) {
e := NoTransitionError{}
if e.Error() != "no transition" {
t.Error("NoTransitionError string mismatch")
}
e.Err = errors.New("no transition")
if e.Error() != "no transition with error: "+e.Err.Error() {
t.Error("NoTransitionError string mismatch")
}
}
func TestCanceledError(t *testing.T) {
e := CanceledError{}
if e.Error() != "transition canceled" {
t.Error("CanceledError string mismatch")
}
e.Err = errors.New("canceled")
if e.Error() != "transition canceled with error: "+e.Err.Error() {
t.Error("CanceledError string mismatch")
}
}
func TestAsyncError(t *testing.T) {
e := AsyncError{}
if e.Error() != "async started" {
t.Error("AsyncError string mismatch")
}
e.Err = errors.New("async")
if e.Error() != "async started with error: "+e.Err.Error() {
t.Error("AsyncError string mismatch")
}
}
func TestInternalError(t *testing.T) {
e := InternalError{}
if e.Error() != "internal error on state transition" {
t.Error("InternalError string mismatch")
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
// Event is the info that get passed as a reference in the callbacks.
type Event struct {
// FSM is a reference to the current FSM.
FSM *FSM
// Event is the event name.
Event string
// Src is the state before the transition.
Src string
// Dst is the state after the transition.
Dst string
// Err is an optional error that can be returned from a callback.
Err error
// Args is a optinal list of arguments passed to the callback.
Args []interface{}
// canceled is an internal flag set if the transition is canceled.
canceled bool
// async is an internal flag set if the transition should be asynchronous
async bool
}
// Cancel can be called in before_<EVENT> or leave_<STATE> to cancel the
// current transition before it happens. It takes an opitonal error, which will
// overwrite e.Err if set before.
func (e *Event) Cancel(err ...error) {
e.canceled = true
if len(err) > 0 {
e.Err = err[0]
}
}
// Async can be called in leave_<STATE> to do an asynchronous state transition.
//
// The current state transition will be on hold in the old state until a final
// call to Transition is made. This will comlete the transition and possibly
// call the other callbacks.
func (e *Event) Async() {
e.async = true
}

View File

@@ -0,0 +1,432 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package fsm implements a finite state machine.
//
// It is heavily based on two FSM implementations:
//
// Javascript Finite State Machine
// https://github.com/jakesgordon/javascript-state-machine
//
// Fysom for Python
// https://github.com/oxplot/fysom (forked at https://github.com/mriehl/fysom)
//
package fsm
import (
"strings"
"sync"
)
// transitioner is an interface for the FSM's transition function.
type transitioner interface {
transition(*FSM) error
}
// FSM is the state machine that holds the current state.
//
// It has to be created with NewFSM to function properly.
type FSM struct {
// current is the state that the FSM is currently in.
current string
// transitions maps events and source states to destination states.
transitions map[eKey]string
// callbacks maps events and targers to callback functions.
callbacks map[cKey]Callback
// transition is the internal transition functions used either directly
// or when Transition is called in an asynchronous state transition.
transition func()
// transitionerObj calls the FSM's transition() function.
transitionerObj transitioner
// stateMu guards access to the current state.
stateMu sync.RWMutex
// eventMu guards access to Event() and Transition().
eventMu sync.Mutex
}
// EventDesc represents an event when initializing the FSM.
//
// The event can have one or more source states that is valid for performing
// the transition. If the FSM is in one of the source states it will end up in
// the specified destination state, calling all defined callbacks as it goes.
type EventDesc struct {
// Name is the event name used when calling for a transition.
Name string
// Src is a slice of source states that the FSM must be in to perform a
// state transition.
Src []string
// Dst is the destination state that the FSM will be in if the transition
// succeds.
Dst string
}
// Callback is a function type that callbacks should use. Event is the current
// event info as the callback happens.
type Callback func(*Event)
// Events is a shorthand for defining the transition map in NewFSM.
type Events []EventDesc
// Callbacks is a shorthand for defining the callbacks in NewFSM.a
type Callbacks map[string]Callback
// NewFSM constructs a FSM from events and callbacks.
//
// The events and transitions are specified as a slice of Event structs
// specified as Events. Each Event is mapped to one or more internal
// transitions from Event.Src to Event.Dst.
//
// Callbacks are added as a map specified as Callbacks where the key is parsed
// as the callback event as follows, and called in the same order:
//
// 1. before_<EVENT> - called before event named <EVENT>
//
// 2. before_event - called before all events
//
// 3. leave_<OLD_STATE> - called before leaving <OLD_STATE>
//
// 4. leave_state - called before leaving all states
//
// 5. enter_<NEW_STATE> - called after entering <NEW_STATE>
//
// 6. enter_state - called after entering all states
//
// 7. after_<EVENT> - called after event named <EVENT>
//
// 8. after_event - called after all events
//
// There are also two short form versions for the most commonly used callbacks.
// They are simply the name of the event or state:
//
// 1. <NEW_STATE> - called after entering <NEW_STATE>
//
// 2. <EVENT> - called after event named <EVENT>
//
// If both a shorthand version and a full version is specified it is undefined
// which version of the callback will end up in the internal map. This is due
// to the psuedo random nature of Go maps. No checking for multiple keys is
// currently performed.
func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) *FSM {
f := &FSM{
transitionerObj: &transitionerStruct{},
current: initial,
transitions: make(map[eKey]string),
callbacks: make(map[cKey]Callback),
}
// Build transition map and store sets of all events and states.
allEvents := make(map[string]bool)
allStates := make(map[string]bool)
for _, e := range events {
for _, src := range e.Src {
f.transitions[eKey{e.Name, src}] = e.Dst
allStates[src] = true
allStates[e.Dst] = true
}
allEvents[e.Name] = true
}
// Map all callbacks to events/states.
for name, fn := range callbacks {
var target string
var callbackType int
switch {
case strings.HasPrefix(name, "before_"):
target = strings.TrimPrefix(name, "before_")
if target == "event" {
target = ""
callbackType = callbackBeforeEvent
} else if _, ok := allEvents[target]; ok {
callbackType = callbackBeforeEvent
}
case strings.HasPrefix(name, "leave_"):
target = strings.TrimPrefix(name, "leave_")
if target == "state" {
target = ""
callbackType = callbackLeaveState
} else if _, ok := allStates[target]; ok {
callbackType = callbackLeaveState
}
case strings.HasPrefix(name, "enter_"):
target = strings.TrimPrefix(name, "enter_")
if target == "state" {
target = ""
callbackType = callbackEnterState
} else if _, ok := allStates[target]; ok {
callbackType = callbackEnterState
}
case strings.HasPrefix(name, "after_"):
target = strings.TrimPrefix(name, "after_")
if target == "event" {
target = ""
callbackType = callbackAfterEvent
} else if _, ok := allEvents[target]; ok {
callbackType = callbackAfterEvent
}
default:
target = name
if _, ok := allStates[target]; ok {
callbackType = callbackEnterState
} else if _, ok := allEvents[target]; ok {
callbackType = callbackAfterEvent
}
}
if callbackType != callbackNone {
f.callbacks[cKey{target, callbackType}] = fn
}
}
return f
}
// Current returns the current state of the FSM.
func (f *FSM) Current() string {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
return f.current
}
// Is returns true if state is the current state.
func (f *FSM) Is(state string) bool {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
return state == f.current
}
// SetState allows the user to move to the given state from current state.
// The call does not trigger any callbacks, if defined.
func (f *FSM) SetState(state string) {
f.stateMu.Lock()
defer f.stateMu.Unlock()
f.current = state
}
// Can returns true if event can occur in the current state.
func (f *FSM) Can(event string) bool {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
_, ok := f.transitions[eKey{event, f.current}]
return ok && (f.transition == nil)
}
// Cannot returns true if event can not occure in the current state.
// It is a convenience method to help code read nicely.
func (f *FSM) Cannot(event string) bool {
return !f.Can(event)
}
// Event initiates a state transition with the named event.
//
// The call takes a variable number of arguments that will be passed to the
// callback, if defined.
//
// It will return nil if the state change is ok or one of these errors:
//
// - event X inappropriate because previous transition did not complete
//
// - event X inappropriate in current state Y
//
// - event X does not exist
//
// - internal error on state transition
//
// The last error should never occur in this situation and is a sign of an
// internal bug.
func (f *FSM) Event(event string, args ...interface{}) error {
f.eventMu.Lock()
defer f.eventMu.Unlock()
f.stateMu.RLock()
defer f.stateMu.RUnlock()
if f.transition != nil {
return InTransitionError{event}
}
dst, ok := f.transitions[eKey{event, f.current}]
if !ok {
for ekey := range f.transitions {
if ekey.event == event {
return InvalidEventError{event, f.current}
}
}
return UnknownEventError{event}
}
e := &Event{f, event, f.current, dst, nil, args, false, false}
err := f.beforeEventCallbacks(e)
if err != nil {
return err
}
if f.current == dst {
f.afterEventCallbacks(e)
return NoTransitionError{e.Err}
}
// Setup the transition, call it later.
f.transition = func() {
f.stateMu.Lock()
f.current = dst
f.stateMu.Unlock()
f.enterStateCallbacks(e)
f.afterEventCallbacks(e)
}
if err = f.leaveStateCallbacks(e); err != nil {
if _, ok := err.(CanceledError); ok {
f.transition = nil
}
return err
}
// Perform the rest of the transition, if not asynchronous.
f.stateMu.RUnlock()
err = f.doTransition()
f.stateMu.RLock()
if err != nil {
return InternalError{}
}
return e.Err
}
// Transition wraps transitioner.transition.
func (f *FSM) Transition() error {
f.eventMu.Lock()
defer f.eventMu.Unlock()
return f.doTransition()
}
// doTransition wraps transitioner.transition.
func (f *FSM) doTransition() error {
return f.transitionerObj.transition(f)
}
// transitionerStruct is the default implementation of the transitioner
// interface. Other implementations can be swapped in for testing.
type transitionerStruct struct{}
// Transition completes an asynchrounous state change.
//
// The callback for leave_<STATE> must prviously have called Async on its
// event to have initiated an asynchronous state transition.
func (t transitionerStruct) transition(f *FSM) error {
if f.transition == nil {
return NotInTransitionError{}
}
f.transition()
f.transition = nil
return nil
}
// beforeEventCallbacks calls the before_ callbacks, first the named then the
// general version.
func (f *FSM) beforeEventCallbacks(e *Event) error {
if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
}
}
if fn, ok := f.callbacks[cKey{"", callbackBeforeEvent}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
}
}
return nil
}
// leaveStateCallbacks calls the leave_ callbacks, first the named then the
// general version.
func (f *FSM) leaveStateCallbacks(e *Event) error {
if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
} else if e.async {
return AsyncError{e.Err}
}
}
if fn, ok := f.callbacks[cKey{"", callbackLeaveState}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
} else if e.async {
return AsyncError{e.Err}
}
}
return nil
}
// enterStateCallbacks calls the enter_ callbacks, first the named then the
// general version.
func (f *FSM) enterStateCallbacks(e *Event) {
if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok {
fn(e)
}
if fn, ok := f.callbacks[cKey{"", callbackEnterState}]; ok {
fn(e)
}
}
// afterEventCallbacks calls the after_ callbacks, first the named then the
// general version.
func (f *FSM) afterEventCallbacks(e *Event) {
if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok {
fn(e)
}
if fn, ok := f.callbacks[cKey{"", callbackAfterEvent}]; ok {
fn(e)
}
}
const (
callbackNone int = iota
callbackBeforeEvent
callbackLeaveState
callbackEnterState
callbackAfterEvent
)
// cKey is a struct key used for keeping the callbacks mapped to a target.
type cKey struct {
// target is either the name of a state or an event depending on which
// callback type the key refers to. It can also be "" for a non-targeted
// callback like before_event.
target string
// callbackType is the situation when the callback will be run.
callbackType int
}
// eKey is a struct key used for storing the transition map.
type eKey struct {
// event is the name of the event that the keys refers to.
event string
// src is the source from where the event can transition.
src string
}

View File

@@ -0,0 +1,791 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
import (
"fmt"
"sync"
"testing"
"time"
)
type fakeTransitionerObj struct {
}
func (t fakeTransitionerObj) transition(f *FSM) error {
return &InternalError{}
}
func TestSameState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "start"},
},
Callbacks{},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestSetState(t *testing.T) {
fsm := NewFSM(
"walking",
Events{
{Name: "walk", Src: []string{"start"}, Dst: "walking"},
},
Callbacks{},
)
fsm.SetState("start")
if fsm.Current() != "start" {
t.Error("expected state to be 'walking'")
}
err := fsm.Event("walk")
if err != nil {
t.Error("transition is expected no error")
}
}
func TestBadTransition(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "running"},
},
Callbacks{},
)
fsm.transitionerObj = new(fakeTransitionerObj)
err := fsm.Event("run")
if err == nil {
t.Error("bad transition should give an error")
}
}
func TestInappropriateEvent(t *testing.T) {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
err := fsm.Event("close")
if e, ok := err.(InvalidEventError); !ok && e.Event != "close" && e.State != "closed" {
t.Error("expected 'InvalidEventError' with correct state and event")
}
}
func TestInvalidEvent(t *testing.T) {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
err := fsm.Event("lock")
if e, ok := err.(UnknownEventError); !ok && e.Event != "close" {
t.Error("expected 'UnknownEventError' with correct event")
}
}
func TestMultipleSources(t *testing.T) {
fsm := NewFSM(
"one",
Events{
{Name: "first", Src: []string{"one"}, Dst: "two"},
{Name: "second", Src: []string{"two"}, Dst: "three"},
{Name: "reset", Src: []string{"one", "two", "three"}, Dst: "one"},
},
Callbacks{},
)
fsm.Event("first")
if fsm.Current() != "two" {
t.Error("expected state to be 'two'")
}
fsm.Event("reset")
if fsm.Current() != "one" {
t.Error("expected state to be 'one'")
}
fsm.Event("first")
fsm.Event("second")
if fsm.Current() != "three" {
t.Error("expected state to be 'three'")
}
fsm.Event("reset")
if fsm.Current() != "one" {
t.Error("expected state to be 'one'")
}
}
func TestMultipleEvents(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "first", Src: []string{"start"}, Dst: "one"},
{Name: "second", Src: []string{"start"}, Dst: "two"},
{Name: "reset", Src: []string{"one"}, Dst: "reset_one"},
{Name: "reset", Src: []string{"two"}, Dst: "reset_two"},
{Name: "reset", Src: []string{"reset_one", "reset_two"}, Dst: "start"},
},
Callbacks{},
)
fsm.Event("first")
fsm.Event("reset")
if fsm.Current() != "reset_one" {
t.Error("expected state to be 'reset_one'")
}
fsm.Event("reset")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
fsm.Event("second")
fsm.Event("reset")
if fsm.Current() != "reset_two" {
t.Error("expected state to be 'reset_two'")
}
fsm.Event("reset")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestGenericCallbacks(t *testing.T) {
beforeEvent := false
leaveState := false
enterState := false
afterEvent := false
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_event": func(e *Event) {
beforeEvent = true
},
"leave_state": func(e *Event) {
leaveState = true
},
"enter_state": func(e *Event) {
enterState = true
},
"after_event": func(e *Event) {
afterEvent = true
},
},
)
fsm.Event("run")
if !(beforeEvent && leaveState && enterState && afterEvent) {
t.Error("expected all callbacks to be called")
}
}
func TestSpecificCallbacks(t *testing.T) {
beforeEvent := false
leaveState := false
enterState := false
afterEvent := false
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
beforeEvent = true
},
"leave_start": func(e *Event) {
leaveState = true
},
"enter_end": func(e *Event) {
enterState = true
},
"after_run": func(e *Event) {
afterEvent = true
},
},
)
fsm.Event("run")
if !(beforeEvent && leaveState && enterState && afterEvent) {
t.Error("expected all callbacks to be called")
}
}
func TestSpecificCallbacksShortform(t *testing.T) {
enterState := false
afterEvent := false
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"end": func(e *Event) {
enterState = true
},
"run": func(e *Event) {
afterEvent = true
},
},
)
fsm.Event("run")
if !(enterState && afterEvent) {
t.Error("expected all callbacks to be called")
}
}
func TestBeforeEventWithoutTransition(t *testing.T) {
beforeEvent := true
fsm := NewFSM(
"start",
Events{
{Name: "dontrun", Src: []string{"start"}, Dst: "start"},
},
Callbacks{
"before_event": func(e *Event) {
beforeEvent = true
},
},
)
err := fsm.Event("dontrun")
if e, ok := err.(NoTransitionError); !ok && e.Err != nil {
t.Error("expected 'NoTransitionError' without custom error")
}
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
if !beforeEvent {
t.Error("expected callback to be called")
}
}
func TestCancelBeforeGenericEvent(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_event": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelBeforeSpecificEvent(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelLeaveGenericState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_state": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelLeaveSpecificState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_start": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelWithError(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_event": func(e *Event) {
e.Cancel(fmt.Errorf("error"))
},
},
)
err := fsm.Event("run")
if _, ok := err.(CanceledError); !ok {
t.Error("expected only 'CanceledError'")
}
if e, ok := err.(CanceledError); ok && e.Err.Error() != "error" {
t.Error("expected 'CanceledError' with correct custom error")
}
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestAsyncTransitionGenericState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_state": func(e *Event) {
e.Async()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
fsm.Transition()
if fsm.Current() != "end" {
t.Error("expected state to be 'end'")
}
}
func TestAsyncTransitionSpecificState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_start": func(e *Event) {
e.Async()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
fsm.Transition()
if fsm.Current() != "end" {
t.Error("expected state to be 'end'")
}
}
func TestAsyncTransitionInProgress(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
{Name: "reset", Src: []string{"end"}, Dst: "start"},
},
Callbacks{
"leave_start": func(e *Event) {
e.Async()
},
},
)
fsm.Event("run")
err := fsm.Event("reset")
if e, ok := err.(InTransitionError); !ok && e.Event != "reset" {
t.Error("expected 'InTransitionError' with correct state")
}
fsm.Transition()
fsm.Event("reset")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestAsyncTransitionNotInProgress(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
{Name: "reset", Src: []string{"end"}, Dst: "start"},
},
Callbacks{},
)
err := fsm.Transition()
if _, ok := err.(NotInTransitionError); !ok {
t.Error("expected 'NotInTransitionError'")
}
}
func TestCallbackNoError(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
},
},
)
e := fsm.Event("run")
if e != nil {
t.Error("expected no error")
}
}
func TestCallbackError(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
e.Err = fmt.Errorf("error")
},
},
)
e := fsm.Event("run")
if e.Error() != "error" {
t.Error("expected error to be 'error'")
}
}
func TestCallbackArgs(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
if len(e.Args) != 1 {
t.Error("too few arguments")
}
arg, ok := e.Args[0].(string)
if !ok {
t.Error("not a string argument")
}
if arg != "test" {
t.Error("incorrect argument")
}
},
},
)
fsm.Event("run", "test")
}
func TestNoDeadLock(t *testing.T) {
var fsm *FSM
fsm = NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
fsm.Current() // Should not result in a panic / deadlock
},
},
)
fsm.Event("run")
}
func TestThreadSafetyRaceCondition(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
},
},
)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = fsm.Current()
}()
fsm.Event("run")
wg.Wait()
}
func TestDoubleTransition(t *testing.T) {
var fsm *FSM
var wg sync.WaitGroup
wg.Add(2)
fsm = NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
wg.Done()
// Imagine a concurrent event coming in of the same type while
// the data access mutex is unlocked because the current transition
// is running its event callbacks, getting around the "active"
// transition checks
if len(e.Args) == 0 {
// Must be concurrent so the test may pass when we add a mutex that synchronizes
// calls to Event(...). It will then fail as an inappropriate transition as we
// have changed state.
go func() {
if err := fsm.Event("run", "second run"); err != nil {
fmt.Println(err)
wg.Done() // It should fail, and then we unfreeze the test.
}
}()
time.Sleep(20 * time.Millisecond)
} else {
panic("Was able to reissue an event mid-transition")
}
},
},
)
if err := fsm.Event("run"); err != nil {
fmt.Println(err)
}
wg.Wait()
}
func TestNoTransition(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "start"},
},
Callbacks{},
)
err := fsm.Event("run")
if _, ok := err.(NoTransitionError); !ok {
t.Error("expected 'NoTransitionError'")
}
}
func ExampleNewFSM() {
fsm := NewFSM(
"green",
Events{
{Name: "warn", Src: []string{"green"}, Dst: "yellow"},
{Name: "panic", Src: []string{"yellow"}, Dst: "red"},
{Name: "panic", Src: []string{"green"}, Dst: "red"},
{Name: "calm", Src: []string{"red"}, Dst: "yellow"},
{Name: "clear", Src: []string{"yellow"}, Dst: "green"},
},
Callbacks{
"before_warn": func(e *Event) {
fmt.Println("before_warn")
},
"before_event": func(e *Event) {
fmt.Println("before_event")
},
"leave_green": func(e *Event) {
fmt.Println("leave_green")
},
"leave_state": func(e *Event) {
fmt.Println("leave_state")
},
"enter_yellow": func(e *Event) {
fmt.Println("enter_yellow")
},
"enter_state": func(e *Event) {
fmt.Println("enter_state")
},
"after_warn": func(e *Event) {
fmt.Println("after_warn")
},
"after_event": func(e *Event) {
fmt.Println("after_event")
},
},
)
fmt.Println(fsm.Current())
err := fsm.Event("warn")
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// green
// before_warn
// before_event
// leave_green
// leave_state
// enter_yellow
// enter_state
// after_warn
// after_event
// yellow
}
func ExampleFSM_Current() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Current())
// Output: closed
}
func ExampleFSM_Is() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Is("closed"))
fmt.Println(fsm.Is("open"))
// Output:
// true
// false
}
func ExampleFSM_Can() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Can("open"))
fmt.Println(fsm.Can("close"))
// Output:
// true
// false
}
func ExampleFSM_Cannot() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Cannot("open"))
fmt.Println(fsm.Cannot("close"))
// Output:
// false
// true
}
func ExampleFSM_Event() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Current())
err := fsm.Event("open")
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
err = fsm.Event("close")
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// closed
// open
// closed
}
func ExampleFSM_Transition() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{
"leave_closed": func(e *Event) {
e.Async()
},
},
)
err := fsm.Event("open")
if e, ok := err.(AsyncError); !ok && e.Err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
err = fsm.Transition()
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// closed
// open
}

View File

@@ -0,0 +1,144 @@
package fsm
// RelationState is relation state type
type RelationState string
// RelationEvent is relation event type
type RelationEvent string
// consts for relation
var (
// states
StateNoRelation = RelationState("no_relation")
StateWhisper = RelationState("whisper")
StateFollowing = RelationState("following")
StateBlacked = RelationState("blacked")
StateFriend = RelationState("friend") // StateFriend is the most special state
// events
EventAddFollowing = RelationEvent("add_following")
EventDelFollowing = RelationEvent("del_following")
EventAddWhisper = RelationEvent("add_whisper")
EventDelWhisper = RelationEvent("del_whisper")
EventAddBlack = RelationEvent("add_black")
EventDelBlack = RelationEvent("del_black")
EventDelFollower = RelationEvent("del_follower")
EventBeFriend = RelationEvent("be_friend") // EventBeFriend is the most special event
)
// RelationEventHandler is used to handle all state change for relation
type RelationEventHandler interface {
AddFollowing(*Event)
DelFollowing(*Event)
AddWhisper(*Event)
DelWhisper(*Event)
AddBlack(*Event)
DelBlack(*Event)
DelFollower(*Event)
}
// DefaultHandler is the default RelationEventHandler
var DefaultHandler = &defaultHandlerImpl{}
type defaultHandlerImpl struct{}
func (*defaultHandlerImpl) AddFollowing(*Event) {}
func (*defaultHandlerImpl) DelFollowing(*Event) {}
func (*defaultHandlerImpl) AddWhisper(*Event) {}
func (*defaultHandlerImpl) DelWhisper(*Event) {}
func (*defaultHandlerImpl) AddBlack(*Event) {}
func (*defaultHandlerImpl) DelBlack(*Event) {}
func (*defaultHandlerImpl) DelFollower(*Event) {}
// RelationStateMachine is used to describe all state change for relation
type RelationStateMachine struct {
*FSM
}
// NewRelationStateMachine will create a RelationStateMachine
func NewRelationStateMachine(initial RelationState, handler RelationEventHandler) *RelationStateMachine {
rs := &RelationStateMachine{
FSM: NewFSM(
string(StateNoRelation),
Events{
{
Name: string(EventAddFollowing),
Src: []string{
string(StateNoRelation),
string(StateWhisper),
string(StateBlacked),
},
Dst: string(StateFollowing),
},
{
Name: string(EventDelFollowing),
Src: []string{
string(StateFollowing),
string(StateFriend),
},
Dst: string(StateNoRelation),
},
{
Name: string(EventAddWhisper),
Src: []string{
string(StateNoRelation),
string(StateFollowing),
string(StateBlacked),
string(StateFriend),
},
Dst: string(StateWhisper),
},
{
Name: string(EventDelWhisper),
Src: []string{
string(StateWhisper),
},
Dst: string(StateNoRelation),
},
{
Name: string(EventAddBlack),
Src: []string{
string(StateNoRelation),
string(StateFollowing),
string(StateFriend),
string(StateWhisper),
},
Dst: string(StateBlacked),
},
{
Name: string(EventDelBlack),
Src: []string{
string(StateBlacked),
},
Dst: string(StateNoRelation),
},
{
Name: string(EventDelBlack),
Src: []string{
string(StateBlacked),
},
Dst: string(StateNoRelation),
},
},
Callbacks{
string(EventAddFollowing): handler.AddFollowing,
string(EventDelFollowing): handler.DelFollowing,
string(EventAddWhisper): handler.AddWhisper,
string(EventDelWhisper): handler.DelWhisper,
string(EventAddBlack): handler.AddBlack,
string(EventDelBlack): handler.DelBlack,
string(EventDelFollower): handler.DelFollower,
},
),
}
return rs
}
// Event is used to execute any events
func (r *RelationStateMachine) Event(event RelationEvent, args ...interface{}) error {
return r.FSM.Event(string(event), args...)
}
// SetState is used to set state
func (r *RelationStateMachine) SetState(state RelationState) {
r.FSM.SetState(string(state))
}

View File

@@ -0,0 +1,16 @@
package fsm
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRelationStateMachine(t *testing.T) {
rs := NewRelationStateMachine(StateNoRelation, DefaultHandler)
assert.NotNil(t, rs)
assert.Equal(t, RelationState(rs.Current()), StateNoRelation)
assert.NoError(t, rs.Event(EventAddFollowing))
assert.Equal(t, StateFollowing, RelationState(rs.Current()))
}

View File

@@ -0,0 +1,45 @@
package fsm
import (
"bytes"
"fmt"
)
// Visualize outputs a visualization of a FSM in Graphviz format.
func Visualize(fsm *FSM) string {
var buf bytes.Buffer
states := make(map[string]int)
buf.WriteString(fmt.Sprintf(`digraph fsm {`))
buf.WriteString("\n")
// make sure the initial state is at top
for k, v := range fsm.transitions {
if k.src == fsm.current {
states[k.src]++
states[v]++
buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event))
buf.WriteString("\n")
}
}
for k, v := range fsm.transitions {
if k.src != fsm.current {
states[k.src]++
states[v]++
buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event))
buf.WriteString("\n")
}
}
buf.WriteString("\n")
for k := range states {
buf.WriteString(fmt.Sprintf(` "%s";`, k))
buf.WriteString("\n")
}
buf.WriteString(fmt.Sprintln("}"))
return buf.String()
}

View File

@@ -0,0 +1,34 @@
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/job/main/relation/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/job/main/relation/conf:go_default_library",
"//app/job/main/relation/service: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,54 @@
package http
import (
"net/http"
"go-common/app/job/main/relation/conf"
"go-common/app/job/main/relation/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
var srv *service.Service
// Init init http service
func Init(c *conf.Config, s *service.Service) {
srv = s
// init inner router
// intM := http.NewServeMux()
// intR := router.New(intM)
// innerRouter(intR)
// // init inner server
// if err := xhttp.Serve(intM, c.MultiHTTP.Inner); err != nil {
// log.Error("xhttp.Serve error(%v)", err)
// panic(err)
// }
innerEngine := bm.DefaultServer(c.BM)
setupInnerEngine(innerEngine)
if err := innerEngine.Start(); err != nil {
panic(err)
}
}
// innerRouter init local router api path.
// func innerRouter(r *router.Router) {
// r.Get("/monitor/ping", ping)
// }
func setupInnerEngine(e *bm.Engine) {
e.Ping(ping)
e.Register(register)
}
func ping(c *bm.Context) {
if err := srv.Ping(c); err != nil {
log.Error("ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}
func register(ctx *bm.Context) {
ctx.JSON(nil, nil)
}

View File

@@ -0,0 +1,45 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"addit.go",
"relation.go",
],
importpath = "go-common/app/job/main/relation/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//app/service/main/relation/model:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/job/main/relation/model/i64b:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["addit_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)

View File

@@ -0,0 +1,58 @@
package model
// AchieveFlag is
type AchieveFlag uint64
// const
var (
EmptyAchieve = AchieveFlag(0)
FollowerAchieve1k = AchieveFlag(1 << 0)
FollowerAchieve5k = AchieveFlag(1 << 1)
FollowerAchieve10k = AchieveFlag(1 << 2)
FollowerAchieve100k = AchieveFlag(1 << 3)
FollowerAchieve1000k = AchieveFlag(1 << 12)
)
// AchieveFromFollower is
func AchieveFromFollower(count int64) AchieveFlag {
if count <= 0 {
return EmptyAchieve
}
if count >= 100000 {
return AchieveFlag(1 << uint64(2+count/100000))
}
if count >= 10000 && count < 100000 {
return FollowerAchieve10k
}
if count >= 5000 && count < 10000 {
return FollowerAchieve5k
}
if count >= 1000 && count < 5000 {
return FollowerAchieve1k
}
return EmptyAchieve
}
// AllAchieveFromFollower is
func AllAchieveFromFollower(count int64) []AchieveFlag {
flags := []AchieveFlag{}
if count <= 0 {
return flags
}
if count >= 1000 {
flags = append(flags, FollowerAchieve1k)
}
if count >= 5000 {
flags = append(flags, FollowerAchieve5k)
}
if count >= 10000 {
flags = append(flags, FollowerAchieve10k)
}
if count >= 100000 {
remain := count / 100000
for i := int64(1); i <= remain; i++ {
flags = append(flags, AchieveFlag(1<<uint64(2+i)))
}
}
return flags
}

View File

@@ -0,0 +1,69 @@
package model
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestDao_AllAchieveFromFollower(t *testing.T) {
Convey("AllAchieveFromFollower", t, func() {
flags := AllAchieveFromFollower(500)
So(flags, ShouldBeEmpty)
flags = AllAchieveFromFollower(1000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k})
flags = AllAchieveFromFollower(2000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k})
flags = AllAchieveFromFollower(5000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k})
flags = AllAchieveFromFollower(5001)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k})
flags = AllAchieveFromFollower(10000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k, FollowerAchieve10k})
flags = AllAchieveFromFollower(100000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k, FollowerAchieve10k, FollowerAchieve10k << 1})
flags = AllAchieveFromFollower(200000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k, FollowerAchieve10k, FollowerAchieve10k << 1, FollowerAchieve10k << 2})
flags = AllAchieveFromFollower(300000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k, FollowerAchieve10k, FollowerAchieve10k << 1, FollowerAchieve10k << 2, FollowerAchieve10k << 3})
flags = AllAchieveFromFollower(305000)
So(flags, ShouldResemble, []AchieveFlag{FollowerAchieve1k, FollowerAchieve5k, FollowerAchieve10k, FollowerAchieve10k << 1, FollowerAchieve10k << 2, FollowerAchieve10k << 3})
})
}
func TestDao_AchieveFromFollower(t *testing.T) {
Convey("AchieveFromFollower", t, func() {
flag := AchieveFromFollower(500)
So(flag, ShouldBeZeroValue)
flag = AchieveFromFollower(1000)
So(flag, ShouldEqual, FollowerAchieve1k)
flag = AchieveFromFollower(2000)
So(flag, ShouldEqual, FollowerAchieve1k)
flag = AchieveFromFollower(5000)
So(flag, ShouldEqual, FollowerAchieve5k)
flag = AchieveFromFollower(10000)
So(flag, ShouldEqual, FollowerAchieve10k)
flag = AchieveFromFollower(100000)
So(flag, ShouldEqual, FollowerAchieve10k<<1)
flag = AchieveFromFollower(200000)
So(flag, ShouldEqual, FollowerAchieve10k<<2)
flag = AchieveFromFollower(305000)
So(flag, ShouldEqual, FollowerAchieve10k<<3)
})
}

View File

@@ -0,0 +1,35 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["xints.go"],
importpath = "go-common/app/job/main/relation/model/i64b",
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["xints_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
)

View File

@@ -0,0 +1,91 @@
package i64b
import (
"database/sql/driver"
"encoding/binary"
)
//Int64Bytes be used to MySql\Protobuf varbinary converting.
type Int64Bytes []int64
// MarshalTo marshal int64 slice to bytes,each int64 will occupy Fixed 8 bytes.
//if the argument data not supplied with the full size,it will return the actual written size
func (is Int64Bytes) MarshalTo(data []byte) (int, error) {
for i, n := range is {
start := i * 8
end := (i + 1) * 8
if len(data) < end {
return start, nil
}
bs := data[start:end]
binary.BigEndian.PutUint64(bs, uint64(n))
}
return 8 * len(is), nil
}
// Size return the total size it will occupy in bytes
func (is Int64Bytes) Size() int {
return len(is) * 8
}
// Unmarshal parse the data into int64 slice
func (is *Int64Bytes) Unmarshal(data []byte) error {
return is.Scan(data)
}
// Scan parse the data into int64 slice
func (is *Int64Bytes) Scan(src interface{}) (err error) {
switch sc := src.(type) {
case []byte:
var res []int64
for i := 0; i < len(sc) && i+8 <= len(sc); i += 8 {
ui := binary.BigEndian.Uint64(sc[i : i+8])
res = append(res, int64(ui))
}
*is = res
}
return
}
// Value marshal int64 slice to driver.Value,each int64 will occupy Fixed 8 bytes
func (is Int64Bytes) Value() (driver.Value, error) {
return is.Bytes(), nil
}
// Bytes marshal int64 slice to bytes,each int64 will occupy Fixed 8 bytes
func (is Int64Bytes) Bytes() []byte {
res := make([]byte, 0, 8*len(is))
for _, i := range is {
bs := make([]byte, 8)
binary.BigEndian.PutUint64(bs, uint64(i))
res = append(res, bs...)
}
return res
}
// Evict get rid of the sepcified num from the slice
func (is *Int64Bytes) Evict(e int64) (ok bool) {
res := make([]int64, len(*is)-1)
for _, v := range *is {
if v != e {
res = append(res, v)
} else {
ok = true
}
}
*is = res
return
}
// Exist judge the sepcified num is in the slice or not
func (is Int64Bytes) Exist(i int64) (e bool) {
for _, v := range is {
if v == i {
e = true
return
}
}
return
}

View File

@@ -0,0 +1,74 @@
package i64b
import (
"testing"
)
func TestMarshalAndUnmarshal(t *testing.T) {
var a = Int64Bytes{1, 2, 3}
data := make([]byte, a.Size())
n, err := a.MarshalTo(data)
if n != 24 {
t.Logf("marshal size must be 24")
t.FailNow()
}
if err != nil {
t.Fatalf("err:%v", err)
}
var b Int64Bytes
err = b.Unmarshal(data)
if err != nil {
t.Fatalf("err:%v", err)
}
if b[0] != 1 || b[1] != 2 || b[2] != 3 {
t.Logf("unmarshal failed!b:%v", b)
t.FailNow()
}
}
func TestUncompleteMarshal(t *testing.T) {
var a = Int64Bytes{1, 2, 3}
data := make([]byte, a.Size()-7)
n, err := a.MarshalTo(data)
if n != 16 {
t.Logf("marshal size must be 16")
t.FailNow()
}
if err != nil {
t.Fatalf("err:%v", err)
}
var b Int64Bytes
err = b.Unmarshal(data)
if err != nil {
t.Fatalf("err:%v", err)
}
if b[0] != 1 || b[1] != 2 {
t.Logf("unmarshal failed!b:%v", b)
t.FailNow()
}
}
func TestNilMarshal(t *testing.T) {
var a = Int64Bytes{1, 2, 3}
var data []byte
n, err := a.MarshalTo(data)
if n != 0 {
t.Logf("marshal size must be 0")
t.FailNow()
}
if err != nil {
t.Fatalf("err:%v", err)
}
var b Int64Bytes
err = b.Unmarshal(data)
if err != nil {
t.Fatalf("err:%v", err)
}
if b != nil {
t.Logf("unmarshal failed!b:%v", b)
t.FailNow()
}
}

View File

@@ -0,0 +1,58 @@
package model
import (
"encoding/json"
"time"
sml "go-common/app/service/main/relation/model"
)
// Message define binlog databus message.
type Message struct {
Action string `json:"action"`
Table string `json:"table"`
New json.RawMessage `json:"new"`
Old json.RawMessage `json:"old"`
}
// Relation user_relation_fid_0~user_relation_fid_49,user_relation_mid_0~user_relation_mid_49
type Relation struct {
Mid int64 `json:"mid,omitempty"`
Fid int64 `json:"fid,omitempty"`
Attribute uint32 `json:"attribute"`
Status int `json:"status"`
MTime string `json:"mtime"`
CTime string `json:"ctime"`
}
// Stat user_relation_stat
type Stat struct {
Mid int64 `json:"mid,omitempty"`
Following int64 `json:"following"`
Whisper int64 `json:"whisper"`
Black int64 `json:"black"`
Follower int64 `json:"follower"`
}
// LastChangeAt is.
func (r *Relation) LastChangeAt() (at time.Time, err error) {
// FIXME(zhoujiahui): ctime and mtime should not be used here
return time.ParseInLocation("2006-01-02 15:04:05", r.MTime, time.Local)
}
// Attr is.
func (r *Relation) Attr() uint32 {
return sml.Attr(r.Attribute)
}
// IsRecent is.
func (r *Relation) IsRecent(at time.Time, trange time.Duration) bool {
lastAt, err := r.LastChangeAt()
if err != nil {
return false
}
if lastAt.Sub(at) > trange {
return true
}
return false
}

View File

@@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["service.go"],
importpath = "go-common/app/job/main/relation/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/job/main/relation/conf:go_default_library",
"//app/job/main/relation/dao:go_default_library",
"//app/job/main/relation/model:go_default_library",
"//app/service/main/relation/model:go_default_library",
"//library/log:go_default_library",
"//library/queue/databus: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"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["service_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/job/main/relation/conf:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,241 @@
package service
import (
"context"
"encoding/json"
"strings"
"sync"
"sync/atomic"
"time"
"go-common/app/job/main/relation/conf"
"go-common/app/job/main/relation/dao"
"go-common/app/job/main/relation/model"
sml "go-common/app/service/main/relation/model"
"go-common/library/log"
"go-common/library/queue/databus"
xtime "go-common/library/time"
)
const (
_relationFidTable = "user_relation_fid_"
_relationMidTable = "user_relation_mid_"
_relationStatTable = "user_relation_stat_"
_relationTagUserTable = "user_relation_tag_user_"
_retry = 5
_retrySleep = time.Second * 1
)
// Service struct of service.
type Service struct {
c *conf.Config
dao *dao.Dao
ds *databus.Databus
waiter *sync.WaitGroup
// monitor
mo int64
// moni *monitor.Service
}
// New create service instance and return.
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
dao: dao.New(c),
ds: databus.New(c.DataBus),
waiter: new(sync.WaitGroup),
// moni: monitor.New(),
}
for i := 0; i < 50; i++ {
s.waiter.Add(1)
go s.subproc()
}
go s.checkConsume()
return
}
func (s *Service) subproc() {
defer s.waiter.Done()
for {
var (
ok bool
err error
res *databus.Message
)
if res, ok = <-s.ds.Messages(); !ok {
log.Error("s.ds.Messages() failed")
return
}
log.Info("received message: res: %+v", res)
mu := &model.Message{}
if err = json.Unmarshal(res.Value, mu); err != nil {
log.Error("go-common/app/job/main/relation,json.Unmarshal (%v) error(%v)", res.Value, err)
continue
}
atomic.AddInt64(&s.mo, 1)
for i := 0; ; i++ {
switch {
case strings.HasPrefix(mu.Table, _relationStatTable):
err = s.stat(mu.Action, mu.New, mu.Old)
case strings.HasPrefix(mu.Table, _relationMidTable):
err = s.relationMid(mu.Action, mu.New, mu.Old)
case strings.HasPrefix(mu.Table, _relationFidTable):
err = s.relationFid(mu.Action, mu.New, mu.Old)
case strings.HasPrefix(mu.Table, _relationTagUserTable):
err = s.tagUser(mu.New)
}
if err != nil {
i++
log.Error("s.flush data(%s) error(%+v)", mu.New, err)
time.Sleep(_retrySleep)
if i > _retry {
// if s.c.Env == "prod" {
// s.moni.Sms(context.TODO(), s.c.Sms.Phone, s.c.Sms.Token, fmt.Sprintf("relation-job update cache fail: %v", err))
// }
break
}
continue
}
break
}
log.Info("consumer action:%v, table:%v,new :%s", mu.Action, mu.Table, mu.New)
res.Commit()
}
}
func (s *Service) tagUser(newMsg []byte) (err error) {
var tags struct {
Fid int64 `json:"fid"`
Mid int64 `json:"mid"`
}
if err = json.Unmarshal(newMsg, &tags); err != nil {
log.Error("json.Unmarshal err(%v)", err)
return
}
f, err := s.dao.UserRelation(context.TODO(), tags.Mid, tags.Fid)
if err != nil || f == nil {
return
}
s.dao.DelTagsCache(context.TODO(), tags.Mid)
s.upFollowingCache(context.TODO(), tags.Mid, f)
return
}
func (s *Service) upFollowingCache(c context.Context, mid int64, f *sml.Following) (err error) {
if f.Attribute == 0 {
s.dao.DelFollowing(c, mid, f)
} else {
if err = s.dao.AddFollowingCache(c, mid, f); err != nil {
return
}
}
if err = s.dao.DelFollowingCache(c, mid); err != nil {
return
}
return s.dao.DelTagCountCache(c, mid)
}
// relationFid
func (s *Service) relationFid(action string, nwMsg []byte, oldMsg []byte) error {
var or *model.Relation
mr := &model.Relation{}
if err := json.Unmarshal(nwMsg, mr); err != nil {
log.Error("json.Unmarshal(%v) error(%v)", nwMsg, err)
return err
}
if len(oldMsg) > 0 {
or = new(model.Relation)
if err := json.Unmarshal(oldMsg, or); err != nil {
log.Error("json.Unmarshal(%v) error(%v)", oldMsg, err)
}
}
// step 1: add notify
// if err := s.dao.AddNotify(context.TODO(), mr.Fid); err != nil {
// log.Error("Failed to s.dao.AddNotify(%v): %+v", mr.Fid, err)
// }
// step 2: handle recent followers
// s.RecentFollowers(action, mr, or)
// step 3: delete cache
return s.dao.DelFollowerCache(mr.Fid)
}
// relationMid
func (s *Service) relationMid(action string, nwMsg []byte, oldMsg []byte) (err error) {
mr := &model.Relation{}
if err = json.Unmarshal(nwMsg, mr); err != nil {
log.Error("json.Unmarshal(%v) error(%v)", nwMsg, err)
return
}
f := &sml.Following{
Mid: mr.Fid,
Attribute: mr.Attribute,
MTime: xtime.Time(time.Now().Unix()),
}
if err = s.upFollowingCache(context.TODO(), mr.Mid, f); err != nil {
return
}
log.Info("Succeed to update following cache: mid: %d: mr: %+v", mr.Mid, mr)
// TODO: del this, just for special attention del old cache.
s.dao.DelTagsCache(context.TODO(), mr.Mid)
log.Info("Succeed to delete tags cache: mid: %d", mr.Mid)
// s.dao.AddNotify(context.TODO(), mr.Mid)
return
}
// stat
func (s *Service) stat(action string, nwMsg []byte, oldMsg []byte) (err error) {
ms := &model.Stat{}
if err = json.Unmarshal(nwMsg, ms); err != nil {
log.Error("json.Unmarshal(%v) error(%v)", nwMsg, err)
return
}
mo := &model.Stat{}
if len(oldMsg) > 0 {
if err = json.Unmarshal(oldMsg, mo); err != nil {
log.Error("json.Unmarshal(%v) error(%v)", oldMsg, err)
err = nil
}
}
if ms.Follower > mo.Follower {
s.dao.FollowerAchieve(context.TODO(), ms.Mid, ms.Follower)
}
return s.dao.DelStatCache(ms.Mid)
}
// check consumer stat
func (s *Service) checkConsume() {
if s.c.Env != "prod" {
return
}
var reMo int64
for {
time.Sleep(1 * time.Minute)
atomic.AddInt64(&s.mo, -reMo)
// if atomic.AddInt64(&s.mo, -reMo) == 0 {
// s.moni.Sms(context.TODO(), s.c.Sms.Phone, s.c.Sms.Token, "relation-job stat did not consume within a minute")
// }
reMo = atomic.LoadInt64(&s.mo)
}
}
// Ping check server ok
func (s *Service) Ping(c context.Context) (err error) {
return s.dao.Ping(c)
}
// Close kafka consumer close.
func (s *Service) Close() (err error) {
s.ds.Close()
return
}
// Wait wait for service exit.
func (s *Service) Wait() {
s.waiter.Wait()
}

View File

@@ -0,0 +1,28 @@
package service
import (
"flag"
"testing"
"go-common/app/job/main/relation/conf"
. "github.com/smartystreets/goconvey/convey"
)
var s *Service
func init() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
s = New(conf.Conf)
}
func TestService_Close(t *testing.T) {
Convey("Close", t, func() {
So(s.Close(), ShouldBeNil)
})
}