26 changed files with 983 additions and 433 deletions
@ -0,0 +1,16 @@ |
|||||||
|
package logic |
||||||
|
|
||||||
|
import "sonet/pkg/utils/logger" |
||||||
|
|
||||||
|
// RoomLoader todo 从持久存储加载用户群关系
|
||||||
|
type RoomLoader struct { |
||||||
|
} |
||||||
|
|
||||||
|
func NewRoomLoader() *RoomLoader { |
||||||
|
return &RoomLoader{} |
||||||
|
} |
||||||
|
|
||||||
|
func (l *RoomLoader) Load(uid string) (groupIds []string, err error) { |
||||||
|
logger.Infof("load %s join rooms", uid) |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,110 @@ |
|||||||
|
package deliver |
||||||
|
|
||||||
|
import ( |
||||||
|
"hash/fnv" |
||||||
|
"sort" |
||||||
|
"strconv" |
||||||
|
) |
||||||
|
|
||||||
|
type PickNode struct { |
||||||
|
Key string |
||||||
|
Weight int |
||||||
|
} |
||||||
|
|
||||||
|
type ConsistentHashPicker struct { |
||||||
|
nodes []*PickNode |
||||||
|
replicas int |
||||||
|
salt string |
||||||
|
hashKeys []uint32 // sorted hashKeys
|
||||||
|
hashNodes map[uint32]*PickNode |
||||||
|
length int |
||||||
|
} |
||||||
|
|
||||||
|
func NewConsistentHashPicker(nodes []*PickNode, replicas int, salt string) *ConsistentHashPicker { |
||||||
|
return &ConsistentHashPicker{ |
||||||
|
nodes: nodes, |
||||||
|
replicas: replicas, |
||||||
|
salt: salt, |
||||||
|
hashKeys: make([]uint32, 0, len(nodes)*replicas), |
||||||
|
hashNodes: make(map[uint32]*PickNode, len(nodes)*replicas), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
DefaultReplicas = 10 |
||||||
|
DefaultSalt = "this_is_salt" |
||||||
|
) |
||||||
|
|
||||||
|
func (c *ConsistentHashPicker) hashFnv32(data []byte) uint32 { |
||||||
|
f := fnv.New32() |
||||||
|
_, err := f.Write(data) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
return f.Sum32() |
||||||
|
} |
||||||
|
|
||||||
|
// Init 构建hash环
|
||||||
|
func (c *ConsistentHashPicker) Init() { |
||||||
|
for _, node := range c.nodes { |
||||||
|
weight := node.Weight |
||||||
|
if weight < 1 { |
||||||
|
weight = 1 |
||||||
|
} |
||||||
|
for i := 0; i < weight; i++ { |
||||||
|
for j := 0; j < c.replicas; j++ { |
||||||
|
key := c.hashFnv32([]byte(strconv.Itoa(i) + node.Key + strconv.Itoa(j) + c.salt)) |
||||||
|
|
||||||
|
if _, ok := c.hashNodes[key]; !ok { |
||||||
|
c.hashKeys = append(c.hashKeys, key) |
||||||
|
} |
||||||
|
c.hashNodes[key] = node |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
sort.Slice(c.hashKeys, func(i, j int) bool { |
||||||
|
return c.hashKeys[i] < c.hashKeys[j] |
||||||
|
}) |
||||||
|
c.length = len(c.hashKeys) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ConsistentHashPicker) Pick(key string) (node *PickNode, ok bool) { |
||||||
|
if c.length == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
hash := c.hashFnv32([]byte(key + c.salt)) |
||||||
|
|
||||||
|
idx := sort.Search(c.length, func(i int) bool { // 二分查找最小为true的index
|
||||||
|
return c.hashKeys[i] >= hash |
||||||
|
}) |
||||||
|
if idx == c.length { |
||||||
|
idx = 0 |
||||||
|
} |
||||||
|
node, ok = c.hashNodes[c.hashKeys[idx]] |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// PickOffset
|
||||||
|
// key: pick key
|
||||||
|
// offset: 顺时针第n个节点
|
||||||
|
// return node: pick node
|
||||||
|
// return same: node是否是offset=0时的相同节点
|
||||||
|
func (c *ConsistentHashPicker) PickOffset(key string, offset int) (node *PickNode, same bool, ok bool) { |
||||||
|
if c.length == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
hash := c.hashFnv32([]byte(key + c.salt)) |
||||||
|
idx := sort.Search(c.length, func(i int) bool { |
||||||
|
return c.hashKeys[i] >= hash |
||||||
|
}) |
||||||
|
if idx == c.length { |
||||||
|
idx = 0 |
||||||
|
} |
||||||
|
hit := (idx + offset) % c.length |
||||||
|
if hit < 0 { |
||||||
|
hit = c.length + hit |
||||||
|
} |
||||||
|
node, ok = c.hashNodes[c.hashKeys[hit]] |
||||||
|
same = c.hashNodes[c.hashKeys[idx]] == node |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
package deliver |
||||||
|
|
||||||
|
import ( |
||||||
|
"strconv" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestConsistentHashPicker(t *testing.T) { |
||||||
|
var nodes []*PickNode |
||||||
|
var hits = make(map[string]int, 10) |
||||||
|
for i := 0; i < 10; i++ { |
||||||
|
node := &PickNode{ |
||||||
|
Key: "node-" + strconv.Itoa(i), |
||||||
|
Weight: 10, |
||||||
|
} |
||||||
|
if i < 5 { |
||||||
|
node.Weight = 5 // 遵循 weight 进行负载均衡
|
||||||
|
} |
||||||
|
nodes = append(nodes, node) |
||||||
|
hits[node.Key] = 0 |
||||||
|
} |
||||||
|
picker := NewConsistentHashPicker(nodes, DefaultReplicas, DefaultSalt) |
||||||
|
picker.Init() |
||||||
|
|
||||||
|
for i := 0; i < 100000; i++ { |
||||||
|
node1, ok1 := picker.Pick(strconv.Itoa(i)) |
||||||
|
node2, ok2 := picker.Pick(strconv.Itoa(i)) |
||||||
|
if !ok1 || !ok2 { |
||||||
|
t.Error("pick non node") |
||||||
|
} |
||||||
|
if node1 != node2 { |
||||||
|
t.Error("pick same key not same node") |
||||||
|
} |
||||||
|
hits[node1.Key]++ |
||||||
|
} |
||||||
|
|
||||||
|
for nodeKey, hit := range hits { |
||||||
|
if hit == 0 { |
||||||
|
t.Errorf("node %s not picked", nodeKey) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
node1, same1, ok1 := picker.PickOffset("100", 1) |
||||||
|
node2, same2, ok2 := picker.PickOffset("100", 1) |
||||||
|
if !ok1 || !ok2 { |
||||||
|
t.Error("pick non node") |
||||||
|
} |
||||||
|
if node1 != node2 || same1 != same2 { |
||||||
|
t.Error("pick same key not same node") |
||||||
|
} |
||||||
|
} |
||||||
@ -1,218 +0,0 @@ |
|||||||
package deliver |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"google.golang.org/grpc" |
|
||||||
"google.golang.org/grpc/credentials/insecure" |
|
||||||
"google.golang.org/protobuf/proto" |
|
||||||
"sonet/api/gen/postal" |
|
||||||
"sonet/pkg/grpc/balancer" |
|
||||||
"sonet/pkg/grpc/discovery" |
|
||||||
"sonet/pkg/plugins/mq" |
|
||||||
"sonet/pkg/protocol/event" |
|
||||||
"sonet/pkg/utils/logger" |
|
||||||
"sync" |
|
||||||
"sync/atomic" |
|
||||||
) |
|
||||||
|
|
||||||
type GroupLoader interface { |
|
||||||
Load(uid string) (groupIds []string, err error) |
|
||||||
} |
|
||||||
|
|
||||||
type GroupDeliver struct { |
|
||||||
svcName string |
|
||||||
groupLoader GroupLoader |
|
||||||
consumer mq.Consumer |
|
||||||
resolver discovery.Resolver |
|
||||||
postal postal.PostalClient // postal consistent hash client
|
|
||||||
directPostalDialOptions []grpc.DialOption |
|
||||||
directPostals *atomic.Value //map[string]*Postal , postal server 直连客户端
|
|
||||||
lock *sync.RWMutex |
|
||||||
} |
|
||||||
|
|
||||||
func NewGroupDeliver( |
|
||||||
msgInServiceName string, |
|
||||||
groupLoader GroupLoader, |
|
||||||
consumer mq.Consumer, |
|
||||||
resolver discovery.Resolver, |
|
||||||
directPostalDialOptions []grpc.DialOption, |
|
||||||
) *GroupDeliver { |
|
||||||
directPostals := &atomic.Value{} |
|
||||||
directPostals.Store(make(map[string]*Postal, 3)) |
|
||||||
|
|
||||||
return &GroupDeliver{ |
|
||||||
svcName: msgInServiceName, |
|
||||||
groupLoader: groupLoader, |
|
||||||
consumer: consumer, |
|
||||||
resolver: resolver, |
|
||||||
directPostalDialOptions: directPostalDialOptions, |
|
||||||
directPostals: directPostals, |
|
||||||
lock: &sync.RWMutex{}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) Init(ctx context.Context, grpcResolver discovery.GrpcResolver, opts ...grpc.DialOption) (err error) { |
|
||||||
err = d.initPostal(ctx, grpcResolver, opts...) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// initial all postal direct clients
|
|
||||||
servers, err := d.resolver.ResolveAll(ctx, postal.Postal_ServiceDesc.ServiceName) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
d.buildPostals(servers) |
|
||||||
|
|
||||||
// watch postal server instance
|
|
||||||
err = d.watchPostal(ctx) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
err = d.subscribe() |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// toPostalGid 加上 svc name 前缀避免和其他服务群组冲突
|
|
||||||
func (d *GroupDeliver) toPostalGid(gid string) string { |
|
||||||
return d.svcName + "." + gid |
|
||||||
} |
|
||||||
|
|
||||||
// subscribe postal online events
|
|
||||||
func (d *GroupDeliver) subscribe() error { |
|
||||||
consumerChannel := fmt.Sprintf("%s:%s", "deliver", d.svcName) |
|
||||||
return d.consumer.Subscribe(event.TopicOnline, consumerChannel, func(message *mq.Message) (err error) { |
|
||||||
online := &event.Online{} |
|
||||||
if e := online.UnmarshalBinary(message.Body); e != nil { |
|
||||||
logger.Error("deliver unmarshal online event payload error: ", e) |
|
||||||
return |
|
||||||
} |
|
||||||
// load uid groups join to postal
|
|
||||||
groupIds, err := d.groupLoader.Load(online.Uid) |
|
||||||
if err != nil { |
|
||||||
logger.Error("deliver load groups error:", err) |
|
||||||
return |
|
||||||
} |
|
||||||
if err = d.GroupJoin(context.Background(), online.Uid, groupIds); err != nil { |
|
||||||
logger.Errorf("deliver uid %s group join error: %v", online.Uid, err) |
|
||||||
return |
|
||||||
} |
|
||||||
return |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) initPostal(ctx context.Context, resolver discovery.GrpcResolver, opts ...grpc.DialOption) (err error) { |
|
||||||
balancer.InitConsistentHashBuilder() |
|
||||||
rb, err := resolver.Resolver() |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
postalUrl := resolver.DialUrl(postal.Postal_ServiceDesc.ServiceName) |
|
||||||
var options []grpc.DialOption |
|
||||||
// consistent hash lb
|
|
||||||
options = append(options, grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, balancer.ConsistentHash))) |
|
||||||
options = append(options, grpc.WithResolvers(rb)) |
|
||||||
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials())) |
|
||||||
options = append(options, opts...) |
|
||||||
conn, err := grpc.DialContext(ctx, postalUrl, options...) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
d.postal = postal.NewPostalClient(conn) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// watchPostal watch postal service list
|
|
||||||
func (d *GroupDeliver) watchPostal(ctx context.Context) (err error) { |
|
||||||
ch, err := d.resolver.Watch(ctx, postal.Postal_ServiceDesc.ServiceName) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
go func() { |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
return |
|
||||||
case servers := <-ch: |
|
||||||
d.buildPostals(servers) |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) buildPostals(servers []discovery.Server) { |
|
||||||
postals := make(map[string]*Postal) |
|
||||||
directPostals := d.directPostals.Load().(map[string]*Postal) |
|
||||||
for _, server := range servers { |
|
||||||
if p, ok := directPostals[server.Addr]; ok { |
|
||||||
postals[server.Addr] = p |
|
||||||
continue |
|
||||||
} |
|
||||||
// new connection
|
|
||||||
conn, err := grpc.DialContext(context.Background(), server.Addr, d.directPostalDialOptions...) |
|
||||||
if err != nil { |
|
||||||
logger.Errorf("dial postal server %+v error: %v", server.Addr, err) |
|
||||||
continue |
|
||||||
} |
|
||||||
postals[server.Addr] = NewPostal(conn) |
|
||||||
} |
|
||||||
|
|
||||||
// close old connection
|
|
||||||
oldPostals := directPostals |
|
||||||
d.directPostals.Store(postals) |
|
||||||
for addr, p := range oldPostals { |
|
||||||
if _, ok := postals[addr]; !ok { |
|
||||||
p.Close() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) DeliverGroup(ctx context.Context, gid string, msg proto.Message, options ...Option) (err error) { |
|
||||||
gid = d.toPostalGid(gid) |
|
||||||
|
|
||||||
// deliver to all postal
|
|
||||||
message, err := protoMessage2Deliver(d.svcName, msg) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
req := postal.ReqDeliverGroup{Gid: gid, Msg: message} |
|
||||||
for _, p := range d.directPostals.Load().(map[string]*Postal) { |
|
||||||
p.DeliverGroup(&req) |
|
||||||
} |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) GroupDissolve(gid string) { |
|
||||||
gid = d.toPostalGid(gid) |
|
||||||
|
|
||||||
req := postal.ReqGroupDissolve{Gid: gid} |
|
||||||
for _, p := range d.directPostals.Load().(map[string]*Postal) { |
|
||||||
p.GroupDissolve(&req) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) GroupJoin(ctx context.Context, uid string, gids []string) (err error) { |
|
||||||
if len(gids) == 0 { |
|
||||||
return |
|
||||||
} |
|
||||||
for i := 0; i < len(gids); i++ { |
|
||||||
gids[i] = d.toPostalGid(gids[i]) |
|
||||||
} |
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, balancer.ConsistentHashKey, uid) |
|
||||||
req := &postal.ReqGroupJoin{Uid: uid, Gids: gids} |
|
||||||
_, err = d.postal.GroupJoin(ctx, req) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func (d *GroupDeliver) GroupLeave(ctx context.Context, uid string, gids []string) (err error) { |
|
||||||
req := &postal.ReqGroupLeave{Uid: uid, Gids: gids} |
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, balancer.ConsistentHashKey, uid) |
|
||||||
_, err = d.postal.GroupLeave(ctx, req) |
|
||||||
return |
|
||||||
} |
|
||||||
@ -0,0 +1,136 @@ |
|||||||
|
package deprecated |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"google.golang.org/grpc" |
||||||
|
"google.golang.org/grpc/credentials/insecure" |
||||||
|
"google.golang.org/protobuf/proto" |
||||||
|
"sonet/api/gen/postal" |
||||||
|
"sonet/pkg/grpc/balancer" |
||||||
|
"sonet/pkg/grpc/discovery" |
||||||
|
"sonet/pkg/protocol/deliver" |
||||||
|
"sonet/pkg/utils/logger" |
||||||
|
) |
||||||
|
|
||||||
|
type Status int16 |
||||||
|
|
||||||
|
const ( |
||||||
|
StatusSuccess Status = 1 |
||||||
|
StatusError Status = 2 |
||||||
|
StatusReceiverOffline Status = 10 |
||||||
|
) |
||||||
|
|
||||||
|
// DDeliver n包,通知消息投递
|
||||||
|
// Deprecated
|
||||||
|
type DDeliver struct { |
||||||
|
svcName string |
||||||
|
postal postal.PostalClient |
||||||
|
} |
||||||
|
|
||||||
|
func NewDDeliver(msgInServiceName string) *DDeliver { |
||||||
|
return &DDeliver{ |
||||||
|
svcName: msgInServiceName, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DDeliver) InitWithResolver(ctx context.Context, resolver discovery.GrpcResolver, opts ...grpc.DialOption) (err error) { |
||||||
|
balancer.InitConsistentHashBuilder() |
||||||
|
rb, err := resolver.Resolver() |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
postalUrl := resolver.DialUrl(postal.Postal_ServiceDesc.ServiceName) |
||||||
|
var options []grpc.DialOption |
||||||
|
// consistent hash lb
|
||||||
|
options = append(options, grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, balancer.ConsistentHash))) |
||||||
|
options = append(options, grpc.WithResolvers(rb)) |
||||||
|
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials())) |
||||||
|
options = append(options, opts...) |
||||||
|
conn, err := grpc.DialContext(ctx, postalUrl, options...) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
d.postal = postal.NewPostalClient(conn) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DDeliver) InitWithAddr(postalAddr string, opts ...grpc.DialOption) (err error) { |
||||||
|
// Conn *grpc.ClientConn
|
||||||
|
var options []grpc.DialOption |
||||||
|
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials())) |
||||||
|
options = append(options, opts...) |
||||||
|
conn, err := grpc.Dial(postalAddr, options...) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
d.postal = postal.NewPostalClient(conn) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DDeliver) Deliver(ctx context.Context, msg proto.Message, receiver string, options ...deliver.Option) (Status, error) { |
||||||
|
return d.deliver0(ctx, msg, []string{receiver}, options...) |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DDeliver) DeliverBatch(ctx context.Context, msg proto.Message, receivers []string, options ...deliver.Option) (Status, error) { |
||||||
|
return d.deliver0(ctx, msg, receivers, options...) |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DDeliver) deliver0(ctx context.Context, msg proto.Message, receivers []string, options ...deliver.Option) (status Status, err error) { |
||||||
|
//opts := deliver.defaultOptions
|
||||||
|
//if options != nil {
|
||||||
|
// for _, opt := range options {
|
||||||
|
// opt.f(&opts)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
if receivers == nil || len(receivers) == 0 { |
||||||
|
// return StatusError, errors.New("receivers is empty")
|
||||||
|
return StatusSuccess, nil |
||||||
|
} |
||||||
|
|
||||||
|
// encode msg
|
||||||
|
message, err := deliver.Proto2DeliverMessage(d.svcName, msg) |
||||||
|
if err != nil { |
||||||
|
return StatusError, err |
||||||
|
} |
||||||
|
|
||||||
|
// deliver to gateway
|
||||||
|
if len(receivers) == 1 { |
||||||
|
// deliver one receiver
|
||||||
|
reqDeliver := &postal.ReqDeliver{ |
||||||
|
Receiver: receivers[0], |
||||||
|
Msg: message, |
||||||
|
} |
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, balancer.ConsistentHashKey, reqDeliver.Receiver) |
||||||
|
res, err := d.postal.Deliver(ctx, reqDeliver) |
||||||
|
if err != nil { |
||||||
|
return StatusError, err |
||||||
|
} |
||||||
|
// TODO res code
|
||||||
|
if res.Ok { |
||||||
|
status = StatusSuccess |
||||||
|
} else { |
||||||
|
status = StatusError |
||||||
|
} |
||||||
|
logger.Info("deliver result: ", err, res) |
||||||
|
} else { |
||||||
|
|
||||||
|
// deliver batch receiver
|
||||||
|
ctx = context.WithValue(ctx, balancer.ConsistentHashKey, receivers[0]) |
||||||
|
req := &postal.ReqDeliverBatch{Receivers: receivers, Msg: message} |
||||||
|
res, err := d.postal.DeliverBatch(ctx, req) |
||||||
|
if err != nil { |
||||||
|
logger.Error("deliver error: ", err) |
||||||
|
return StatusError, err |
||||||
|
} |
||||||
|
if res.Ok { |
||||||
|
status = StatusSuccess |
||||||
|
} else { |
||||||
|
status = StatusError |
||||||
|
} |
||||||
|
logger.Info("deliver batch result: ", res) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,140 @@ |
|||||||
|
package deliver |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"google.golang.org/protobuf/proto" |
||||||
|
"sonet/api/gen/postal" |
||||||
|
"sonet/pkg/plugins/mq" |
||||||
|
"sonet/pkg/protocol/event" |
||||||
|
"sonet/pkg/utils/collect" |
||||||
|
"sonet/pkg/utils/logger" |
||||||
|
) |
||||||
|
|
||||||
|
type GroupLoader interface { |
||||||
|
Load(uid string) (groupIds []string, err error) |
||||||
|
} |
||||||
|
|
||||||
|
type GroupDeliver struct { |
||||||
|
svcName string |
||||||
|
groupLoader GroupLoader |
||||||
|
consumer mq.Consumer |
||||||
|
postalPicker *PostalPicker |
||||||
|
} |
||||||
|
|
||||||
|
func NewGroupDeliver( |
||||||
|
msgInServiceName string, |
||||||
|
groupLoader GroupLoader, |
||||||
|
consumer mq.Consumer, |
||||||
|
) *GroupDeliver { |
||||||
|
return &GroupDeliver{ |
||||||
|
svcName: msgInServiceName, |
||||||
|
groupLoader: groupLoader, |
||||||
|
consumer: consumer, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *GroupDeliver) Init(ctx context.Context) (err error) { |
||||||
|
err = d.subscribe() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// toPostalGid 加上 svc name 前缀避免和其他服务群组冲突
|
||||||
|
func (d *GroupDeliver) toPostalGid(gid string) string { |
||||||
|
return d.svcName + "." + gid |
||||||
|
} |
||||||
|
|
||||||
|
// subscribe postal online events
|
||||||
|
func (d *GroupDeliver) subscribe() error { |
||||||
|
consumerChannel := fmt.Sprintf("%s:%s", "deliver", d.svcName) |
||||||
|
return d.consumer.Subscribe(event.TopicOnline, consumerChannel, func(message *mq.Message) (err error) { |
||||||
|
online := &event.Online{} |
||||||
|
if e := online.UnmarshalBinary(message.Body); e != nil { |
||||||
|
logger.Error("deliver unmarshal online event payload error: ", e) |
||||||
|
return |
||||||
|
} |
||||||
|
// load uid groups join to postal
|
||||||
|
groupIds, err := d.groupLoader.Load(online.Uid) |
||||||
|
if err != nil { |
||||||
|
logger.Error("deliver load groups error:", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if err = d.GroupJoin(context.Background(), online.Uid, groupIds); err != nil { |
||||||
|
logger.Errorf("deliver uid %s group join error: %v", online.Uid, err) |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// DeliverGroup 群组消息广播投递到所有 postal 节点
|
||||||
|
func (d *GroupDeliver) DeliverGroup(ctx context.Context, gid string, msg proto.Message, options ...Option) (err error) { |
||||||
|
message, err := Proto2DeliverMessage(d.svcName, msg) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
clients := d.postalPicker.PickAll() |
||||||
|
|
||||||
|
var errs []error |
||||||
|
req := postal.ReqDeliverGroup{Gid: d.toPostalGid(gid), Msg: message} |
||||||
|
for _, client := range clients { |
||||||
|
_, err := client.DeliverGroup(ctx, &req) |
||||||
|
if err != nil { |
||||||
|
errs = append(errs, err) |
||||||
|
} |
||||||
|
} |
||||||
|
if len(errs) > 0 { |
||||||
|
err = errors.Join(errs...) |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// GroupDissolve 群组消息广播投递到所有 postal 节点
|
||||||
|
func (d *GroupDeliver) GroupDissolve(ctx context.Context, gid string) (err error) { |
||||||
|
clients := d.postalPicker.PickAll() |
||||||
|
|
||||||
|
var errs []error |
||||||
|
req := postal.ReqGroupDissolve{Gid: d.toPostalGid(gid)} |
||||||
|
for _, client := range clients { |
||||||
|
_, err := client.GroupDissolve(ctx, &req) |
||||||
|
if err != nil { |
||||||
|
errs = append(errs, err) |
||||||
|
} |
||||||
|
} |
||||||
|
if len(errs) > 0 { |
||||||
|
err = errors.Join(errs...) |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (d *GroupDeliver) GroupJoin(ctx context.Context, uid string, gids []string) (err error) { |
||||||
|
if len(gids) == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
gids = collect.Mapping(gids, d.toPostalGid) |
||||||
|
|
||||||
|
client, err := d.postalPicker.Pick(uid) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
_, err = client.GroupJoin(ctx, &postal.ReqGroupJoin{Uid: uid, Gids: gids}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (d *GroupDeliver) GroupLeave(ctx context.Context, uid string, gids []string) (err error) { |
||||||
|
if len(gids) == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
gids = collect.Mapping(gids, d.toPostalGid) |
||||||
|
|
||||||
|
client, err := d.postalPicker.Pick(uid) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
req := &postal.ReqGroupLeave{Uid: uid, Gids: gids} |
||||||
|
_, err = client.GroupLeave(ctx, req) |
||||||
|
return |
||||||
|
} |
||||||
@ -1,45 +0,0 @@ |
|||||||
package deliver |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"google.golang.org/grpc" |
|
||||||
"sonet/api/gen/postal" |
|
||||||
"sonet/pkg/utils/logger" |
|
||||||
) |
|
||||||
|
|
||||||
type Postal struct { |
|
||||||
conn *grpc.ClientConn |
|
||||||
client postal.PostalClient |
|
||||||
} |
|
||||||
|
|
||||||
func NewPostal(conn *grpc.ClientConn) *Postal { |
|
||||||
return &Postal{ |
|
||||||
conn: conn, |
|
||||||
client: postal.NewPostalClient(conn), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *Postal) Close() { |
|
||||||
err := p.conn.Close() |
|
||||||
if err != nil { |
|
||||||
logger.Error("postal close error: ", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// DeliverGroup 群消息发送
|
|
||||||
func (p *Postal) DeliverGroup(req *postal.ReqDeliverGroup) { |
|
||||||
// todo put in chan
|
|
||||||
res, err := p.client.DeliverGroup(context.Background(), req) |
|
||||||
if err != nil || !res.Ok { |
|
||||||
logger.Errorf("deliver to postal failed: %+v, %v", res, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// GroupDissolve 解散群组
|
|
||||||
func (p *Postal) GroupDissolve(req *postal.ReqGroupDissolve) { |
|
||||||
// todo put in chan
|
|
||||||
_, err := p.client.GroupDissolve(context.Background(), req) |
|
||||||
if err != nil { |
|
||||||
logger.Errorf("group dissolve failed: %v", err) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,40 @@ |
|||||||
|
package deliver |
||||||
|
|
||||||
|
import ( |
||||||
|
"google.golang.org/grpc" |
||||||
|
"sonet/api/gen/postal" |
||||||
|
"sonet/pkg/utils/logger" |
||||||
|
) |
||||||
|
|
||||||
|
type PostalNode struct { |
||||||
|
addr string |
||||||
|
conn *grpc.ClientConn |
||||||
|
client postal.PostalClient |
||||||
|
} |
||||||
|
|
||||||
|
func NewPostalNode(addr string, conn *grpc.ClientConn) *PostalNode { |
||||||
|
return &PostalNode{ |
||||||
|
addr: addr, |
||||||
|
conn: conn, |
||||||
|
client: postal.NewPostalClient(conn), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalNode) Close() { |
||||||
|
err := p.conn.Close() |
||||||
|
if err != nil { |
||||||
|
logger.Error("postal close error: ", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalNode) GetAddr() string { |
||||||
|
return p.addr |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalNode) GetConn() *grpc.ClientConn { |
||||||
|
return p.conn |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalNode) GetClient() postal.PostalClient { |
||||||
|
return p.client |
||||||
|
} |
||||||
@ -0,0 +1,172 @@ |
|||||||
|
package deliver |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"google.golang.org/grpc" |
||||||
|
"sonet/api/gen/postal" |
||||||
|
"sonet/pkg/grpc/discovery" |
||||||
|
"sonet/pkg/utils/collect" |
||||||
|
"sonet/pkg/utils/logger" |
||||||
|
"sync/atomic" |
||||||
|
) |
||||||
|
|
||||||
|
type PostalPicker struct { |
||||||
|
resolver discovery.Resolver |
||||||
|
directPostalDialOptions []grpc.DialOption |
||||||
|
directPostalNodes atomic.Value // map[string]*PostalNode , postal server 直连客户端
|
||||||
|
picker atomic.Value // *ConsistentHashPicker
|
||||||
|
} |
||||||
|
|
||||||
|
func NewPostalPicker(resolver discovery.Resolver, directPostalDialOptions ...grpc.DialOption) *PostalPicker { |
||||||
|
return &PostalPicker{ |
||||||
|
resolver: resolver, |
||||||
|
directPostalDialOptions: directPostalDialOptions, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalPicker) Init(ctx context.Context) (err error) { |
||||||
|
// initial all postal direct clients
|
||||||
|
servers, err := p.resolver.ResolveAll(ctx, postal.Postal_ServiceDesc.ServiceName) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
p.newPostalServers(servers) |
||||||
|
|
||||||
|
// watch postal server instance
|
||||||
|
err = p.watchPostal(ctx) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// watchPostal watch postal service list
|
||||||
|
func (p *PostalPicker) watchPostal(ctx context.Context) (err error) { |
||||||
|
ch, err := p.resolver.Watch(ctx, postal.Postal_ServiceDesc.ServiceName) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-ctx.Done(): |
||||||
|
return |
||||||
|
case servers := <-ch: |
||||||
|
p.newPostalServers(servers) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// newPostalServers postal集群节点增减
|
||||||
|
func (p *PostalPicker) newPostalServers(servers []discovery.Server) { |
||||||
|
newPostal := make(map[string]*PostalNode) |
||||||
|
var pickServers []discovery.Server |
||||||
|
oldPostal, ok := p.directPostalNodes.Load().(map[string]*PostalNode) |
||||||
|
if !ok { |
||||||
|
oldPostal = make(map[string]*PostalNode) |
||||||
|
} |
||||||
|
|
||||||
|
for _, server := range servers { |
||||||
|
if p, ok := oldPostal[server.Addr]; ok { |
||||||
|
newPostal[server.Addr] = p |
||||||
|
continue |
||||||
|
} |
||||||
|
// new connection
|
||||||
|
conn, err := grpc.DialContext(context.Background(), server.Addr, p.directPostalDialOptions...) |
||||||
|
if err != nil { |
||||||
|
logger.Errorf("dial postal server %+v error: %v", server, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
newPostal[server.Addr] = NewPostalNode(server.Addr, conn) |
||||||
|
pickServers = append(pickServers, server) |
||||||
|
} |
||||||
|
// 重新构建 consistent hash picker
|
||||||
|
p.newConsistentHash(pickServers) |
||||||
|
|
||||||
|
// close old connection
|
||||||
|
p.directPostalNodes.Store(newPostal) |
||||||
|
for addr, p := range oldPostal { |
||||||
|
if _, ok := newPostal[addr]; !ok { |
||||||
|
p.Close() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalPicker) newConsistentHash(servers []discovery.Server) { |
||||||
|
nodes := collect.Mapping(servers, func(server discovery.Server) *PickNode { |
||||||
|
return &PickNode{ |
||||||
|
Key: server.Addr, |
||||||
|
Weight: server.GetWeight(), |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
picker := NewConsistentHashPicker(nodes, DefaultReplicas, DefaultSalt) |
||||||
|
picker.Init() |
||||||
|
p.picker.Store(picker) |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalPicker) Pick(key string) (client postal.PostalClient, err error) { |
||||||
|
postalNode, err := p.PickNode(key) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
client = postalNode.client |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalPicker) PickNode(key string) (postalNode *PostalNode, err error) { |
||||||
|
picker := p.picker.Load().(*ConsistentHashPicker) |
||||||
|
node, ok := picker.Pick(key) |
||||||
|
if !ok { |
||||||
|
err = errors.New("no available postal server") |
||||||
|
return |
||||||
|
} |
||||||
|
postalNode, ok = p.directPostalNodes.Load().(map[string]*PostalNode)[node.Key] |
||||||
|
if !ok { |
||||||
|
err = fmt.Errorf("no conn for postal server %s", node.Key) |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// PickOffset
|
||||||
|
// return same: client 是否与 offset=0 时相同
|
||||||
|
func (p *PostalPicker) PickOffset(key string, offset int) (client postal.PostalClient, same bool, err error) { |
||||||
|
postalNode, same, err := p.PickOffsetNode(key, offset) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
client = postalNode.client |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalPicker) PickOffsetNode(key string, offset int) (postalNode *PostalNode, same bool, err error) { |
||||||
|
picker := p.picker.Load().(*ConsistentHashPicker) |
||||||
|
node, same, ok := picker.PickOffset(key, offset) |
||||||
|
if !ok { |
||||||
|
err = errors.New("no available postal server") |
||||||
|
return |
||||||
|
} |
||||||
|
postalNode, ok = p.directPostalNodes.Load().(map[string]*PostalNode)[node.Key] |
||||||
|
if !ok { |
||||||
|
err = fmt.Errorf("no conn for postal server %s", node.Key) |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PostalPicker) PickAll() (clients []postal.PostalClient) { |
||||||
|
nodes, ok := p.directPostalNodes.Load().(map[string]*PostalNode) |
||||||
|
if !ok { |
||||||
|
return |
||||||
|
} |
||||||
|
clients = make([]postal.PostalClient, 0, len(nodes)) |
||||||
|
for _, node := range nodes { |
||||||
|
clients = append(clients, node.client) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
package nets |
||||||
|
|
||||||
|
import "testing" |
||||||
|
|
||||||
|
func TestIpConvert(t *testing.T) { |
||||||
|
ip := "192.168.1.110" |
||||||
|
// ip := "255.255.255.255"
|
||||||
|
i64, err := Ip2i64(ip) |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
ip2 := I642Ip(int64(int32(i64))) |
||||||
|
if ip2 != ip { |
||||||
|
t.Error("ip parse error") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAddressConvert(t *testing.T) { |
||||||
|
addr := "192.168.1.110:8080" |
||||||
|
i64, err := Address2i64(addr) |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
addr2 := I642Address(i64) |
||||||
|
if addr2 != addr { |
||||||
|
t.Error("address parse error") |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue