mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-13 12:43:45 +08:00
tt
This commit is contained in:
246
hotgo-server/app/factory/queue/kafkamq.go
Normal file
246
hotgo-server/app/factory/queue/kafkamq.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/bufanyun/hotgo/app/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KafkaMq struct {
|
||||
endPoints []string
|
||||
Partitions int32
|
||||
producerIns sarama.AsyncProducer
|
||||
consumerIns sarama.ConsumerGroup
|
||||
}
|
||||
|
||||
type KafkaConfig struct {
|
||||
ClientId string
|
||||
Brokers []string
|
||||
GroupID string
|
||||
Partitions int32
|
||||
Replication int16
|
||||
Version string
|
||||
UserName string
|
||||
Password string
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// SendMsg 按字符串类型生产数据
|
||||
func (r *KafkaMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
|
||||
return r.SendByteMsg(topic, []byte(body))
|
||||
}
|
||||
|
||||
// SendByteMsg 生产数据
|
||||
func (r *KafkaMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error) {
|
||||
msg := &sarama.ProducerMessage{
|
||||
Topic: topic,
|
||||
Value: sarama.ByteEncoder(body),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if r.producerIns == nil {
|
||||
return mqMsg, gerror.New("queue kafka producerIns is nil")
|
||||
}
|
||||
|
||||
r.producerIns.Input() <- msg
|
||||
ctx, cancle := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancle()
|
||||
|
||||
select {
|
||||
case info := <-r.producerIns.Successes():
|
||||
return MqMsg{
|
||||
RunType: SendMsg,
|
||||
Topic: info.Topic,
|
||||
Offset: info.Offset,
|
||||
Partition: info.Partition,
|
||||
Timestamp: info.Timestamp,
|
||||
}, nil
|
||||
case fail := <-r.producerIns.Errors():
|
||||
if nil != fail {
|
||||
return mqMsg, fail.Err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return mqMsg, gerror.New("send mqMst timeout")
|
||||
}
|
||||
|
||||
return mqMsg, nil
|
||||
}
|
||||
|
||||
// ListenReceiveMsgDo 消费数据
|
||||
func (r *KafkaMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
|
||||
if r.consumerIns == nil {
|
||||
return gerror.New("queue kafka consumer not register")
|
||||
}
|
||||
|
||||
consumer := Consumer{
|
||||
ready: make(chan bool),
|
||||
receiveDoFun: receiveDo,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
|
||||
for {
|
||||
if err := r.consumerIns.Consume(ctx, []string{topic}, &consumer); err != nil {
|
||||
FatalLog(ctx, "kafka Error from consumer", err)
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
Log(ctx, fmt.Sprint("kafka consoumer stop : %v", ctx.Err()))
|
||||
return
|
||||
}
|
||||
consumer.ready = make(chan bool)
|
||||
}
|
||||
}()
|
||||
|
||||
<-consumer.ready // Await till the consumer has been set up
|
||||
Log(ctx, "kafka consumer up and running!...")
|
||||
|
||||
utils.Signal.AppDefer(func() {
|
||||
Log(ctx, "kafka consumer close...")
|
||||
cancel()
|
||||
if err = r.consumerIns.Close(); err != nil {
|
||||
FatalLog(ctx, "kafka Error closing client", err)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RegisterRedisMqConsumerMust 注册消费者
|
||||
func RegisterKafkaMqConsumerMust(connOpt KafkaConfig) (client MqConsumer) {
|
||||
mqIns := &KafkaMq{}
|
||||
|
||||
kfkVersion, _ := sarama.ParseKafkaVersion(connOpt.Version)
|
||||
if validateVersion(kfkVersion) == false {
|
||||
kfkVersion = sarama.V2_4_0_0
|
||||
}
|
||||
|
||||
brokers := connOpt.Brokers
|
||||
config := sarama.NewConfig()
|
||||
config.Consumer.Return.Errors = true
|
||||
config.Version = kfkVersion
|
||||
if connOpt.UserName != "" {
|
||||
config.Net.SASL.Enable = true
|
||||
config.Net.SASL.User = connOpt.UserName
|
||||
config.Net.SASL.Password = connOpt.Password
|
||||
}
|
||||
|
||||
// 默认按随机方式消费
|
||||
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
|
||||
config.Consumer.Offsets.Initial = sarama.OffsetNewest
|
||||
config.Consumer.Offsets.AutoCommit.Interval = 10 * time.Millisecond
|
||||
config.ClientID = connOpt.ClientId
|
||||
|
||||
consumerClient, err := sarama.NewConsumerGroup(brokers, connOpt.GroupID, config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mqIns.consumerIns = consumerClient
|
||||
return mqIns
|
||||
}
|
||||
|
||||
// RegisterKafkaProducerMust 注册并启动生产者接口实现
|
||||
func RegisterKafkaProducerMust(connOpt KafkaConfig) (client MqProducer) {
|
||||
mqIns := &KafkaMq{}
|
||||
|
||||
connOpt.ClientId = "HOTGO-Producer"
|
||||
RegisterKafkaProducer(connOpt, mqIns) //这里如果使用go程需要处理chan同步问题
|
||||
|
||||
return mqIns
|
||||
}
|
||||
|
||||
// RegisterKafkaProducerAsync 注册同步类型实例
|
||||
func RegisterKafkaProducer(connOpt KafkaConfig, mqIns *KafkaMq) {
|
||||
kfkVersion, _ := sarama.ParseKafkaVersion(connOpt.Version)
|
||||
if validateVersion(kfkVersion) == false {
|
||||
kfkVersion = sarama.V2_4_0_0
|
||||
}
|
||||
|
||||
brokers := connOpt.Brokers
|
||||
config := sarama.NewConfig()
|
||||
// 等待服务器所有副本都保存成功后的响应
|
||||
config.Producer.RequiredAcks = sarama.WaitForAll
|
||||
// 随机向partition发送消息
|
||||
config.Producer.Partitioner = sarama.NewRandomPartitioner
|
||||
// 是否等待成功和失败后的响应,只有上面的RequireAcks设置不是NoReponse这里才有用.
|
||||
config.Producer.Return.Successes = true
|
||||
|
||||
config.Producer.Return.Errors = true
|
||||
config.Producer.Compression = sarama.CompressionNone
|
||||
config.ClientID = connOpt.ClientId
|
||||
|
||||
config.Version = kfkVersion
|
||||
if connOpt.UserName != "" {
|
||||
config.Net.SASL.Enable = true
|
||||
config.Net.SASL.User = connOpt.UserName
|
||||
config.Net.SASL.Password = connOpt.Password
|
||||
}
|
||||
|
||||
var err error
|
||||
mqIns.producerIns, err = sarama.NewAsyncProducer(brokers, config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
utils.Signal.AppDefer(func() {
|
||||
Log(ctx, "kafka producer AsyncClose...")
|
||||
mqIns.producerIns.AsyncClose()
|
||||
})
|
||||
}
|
||||
|
||||
// validateVersion 验证版本是否有效
|
||||
func validateVersion(version sarama.KafkaVersion) bool {
|
||||
for _, item := range sarama.SupportedVersions {
|
||||
if version.String() == item.String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Consumer struct {
|
||||
ready chan bool
|
||||
receiveDoFun func(mqMsg MqMsg)
|
||||
}
|
||||
|
||||
// Setup is run at the beginning of a new session, before ConsumeClaim
|
||||
func (consumer *Consumer) Setup(sarama.ConsumerGroupSession) error {
|
||||
// Mark the consumer as ready
|
||||
close(consumer.ready)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
|
||||
func (consumer *Consumer) Cleanup(sarama.ConsumerGroupSession) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().
|
||||
func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
|
||||
|
||||
// NOTE:
|
||||
// Do not move the code below to a goroutine.
|
||||
// The `ConsumeClaim` itself is called within a goroutine, see:
|
||||
// https://github.com/Shopify/sarama/blob/master/consumer_group.go#L27-L29
|
||||
// `ConsumeClaim` 方法已经是 goroutine 调用 不要在该方法内进行 goroutine
|
||||
for message := range claim.Messages() {
|
||||
consumer.receiveDoFun(MqMsg{
|
||||
RunType: ReceiveMsg,
|
||||
Topic: message.Topic,
|
||||
Body: message.Value,
|
||||
Offset: message.Offset,
|
||||
Timestamp: message.Timestamp,
|
||||
Partition: message.Partition,
|
||||
})
|
||||
session.MarkMessage(message, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
63
hotgo-server/app/factory/queue/list.go
Normal file
63
hotgo-server/app/factory/queue/list.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Queue struct {
|
||||
l *list.List
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func NewQueue() *Queue {
|
||||
return &Queue{l: list.New()}
|
||||
}
|
||||
|
||||
func (q *Queue) LPush(v interface{}) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
q.m.Lock()
|
||||
defer q.m.Unlock()
|
||||
q.l.PushFront(v)
|
||||
}
|
||||
|
||||
func (q *Queue) RPush(v interface{}) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
q.m.Lock()
|
||||
defer q.m.Unlock()
|
||||
q.l.PushBack(v)
|
||||
}
|
||||
|
||||
func (q *Queue) LPop() interface{} {
|
||||
q.m.Lock()
|
||||
defer q.m.Unlock()
|
||||
|
||||
element := q.l.Front()
|
||||
if element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
q.l.Remove(element)
|
||||
return element.Value
|
||||
}
|
||||
|
||||
func (q *Queue) RPop() interface{} {
|
||||
q.m.Lock()
|
||||
defer q.m.Unlock()
|
||||
|
||||
element := q.l.Back()
|
||||
if element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
q.l.Remove(element)
|
||||
return element.Value
|
||||
}
|
||||
|
||||
func (q *Queue) Len() int {
|
||||
return q.l.Len()
|
||||
}
|
||||
39
hotgo-server/app/factory/queue/logger.go
Normal file
39
hotgo-server/app/factory/queue/logger.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/bufanyun/hotgo/app/consts"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
|
||||
// 消费日志
|
||||
func ConsumerLog(ctx context.Context, topic string, mqMsg MqMsg, err error) {
|
||||
|
||||
if err != nil {
|
||||
g.Log(consts.QueueLogPath).Error(ctx, "消费 ["+topic+"] 失败", mqMsg, err)
|
||||
} else {
|
||||
g.Log(consts.QueueLogPath).Print(ctx, "消费 ["+topic+"] 成功", mqMsg.MsgId)
|
||||
}
|
||||
}
|
||||
|
||||
// 生产日志
|
||||
func ProducerLog(ctx context.Context, topic string, data interface{}, err error) {
|
||||
|
||||
if err != nil {
|
||||
g.Log(consts.QueueLogPath).Error(ctx, "生产 ["+topic+"] 失败", gconv.String(data))
|
||||
} else {
|
||||
g.Log(consts.QueueLogPath).Print(ctx, "生产 ["+topic+"] 成功", gconv.String(data))
|
||||
}
|
||||
}
|
||||
|
||||
// 致命日志
|
||||
func FatalLog(ctx context.Context, text string, err error) {
|
||||
g.Log(consts.QueueLogPath).Fatal(ctx, text+":", err)
|
||||
}
|
||||
|
||||
// 通用
|
||||
func Log(ctx context.Context, text string) {
|
||||
g.Log(consts.QueueLogPath).Print(ctx, text)
|
||||
}
|
||||
248
hotgo-server/app/factory/queue/main.go
Normal file
248
hotgo-server/app/factory/queue/main.go
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package queue
|
||||
|
||||
import (
|
||||
"github.com/bufanyun/hotgo/app/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//
|
||||
// MqProducer
|
||||
// @Description
|
||||
//
|
||||
type MqProducer interface {
|
||||
SendMsg(topic string, body string) (mqMsg MqMsg, err error)
|
||||
SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error)
|
||||
}
|
||||
|
||||
//
|
||||
// MqConsumer
|
||||
// @Description
|
||||
//
|
||||
type MqConsumer interface {
|
||||
ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error)
|
||||
}
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
SendMsg
|
||||
ReceiveMsg
|
||||
)
|
||||
|
||||
type MqMsg struct {
|
||||
RunType int `json:"run_type"`
|
||||
Topic string `json:"topic"`
|
||||
MsgId string `json:"msg_id"`
|
||||
Offset int64 `json:"offset"`
|
||||
Partition int32 `json:"partition"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
Body []byte `json:"body"`
|
||||
}
|
||||
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
mqProducerInstanceMap map[string]MqProducer
|
||||
mqConsumerInstanceMap map[string]MqConsumer
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
mqProducerInstanceMap = make(map[string]MqProducer)
|
||||
mqConsumerInstanceMap = make(map[string]MqConsumer)
|
||||
}
|
||||
|
||||
//
|
||||
// @Title 实例化消费者
|
||||
// @Description
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @Return mqClient
|
||||
// @Return err
|
||||
//
|
||||
func InstanceConsumer() (mqClient MqConsumer, err error) {
|
||||
groupName, _ := g.Cfg().Get(ctx, "queue.groupName", "hotgo")
|
||||
return NewConsumer(groupName.String())
|
||||
}
|
||||
|
||||
//
|
||||
// @Title 实例化生产者
|
||||
// @Description
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @Return mqClient
|
||||
// @Return err
|
||||
//
|
||||
func InstanceProducer() (mqClient MqProducer, err error) {
|
||||
groupName, _ := g.Cfg().Get(ctx, "queue.groupName", "hotgo")
|
||||
return NewProducer(groupName.String())
|
||||
}
|
||||
|
||||
//
|
||||
// @Title 新建一个生产者实例
|
||||
// @Description
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @Param groupName
|
||||
// @Return mqClient
|
||||
// @Return err
|
||||
//
|
||||
func NewProducer(groupName string) (mqClient MqProducer, err error) {
|
||||
if item, ok := mqProducerInstanceMap[groupName]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
if groupName == "" {
|
||||
return mqClient, gerror.New("mq groupName is empty.")
|
||||
}
|
||||
|
||||
// 驱动
|
||||
driver, _ := g.Cfg().Get(ctx, "queue.driver", "")
|
||||
|
||||
// 重试次数
|
||||
retryCount, _ := g.Cfg().Get(ctx, "queue.retry", 2)
|
||||
retry := retryCount.Int()
|
||||
|
||||
switch driver.String() {
|
||||
case "rocketmq":
|
||||
address, _ := g.Cfg().Get(ctx, "queue.rocketmq.address", nil)
|
||||
if len(address.Strings()) == 0 {
|
||||
panic("queue rocketmq address is not support")
|
||||
}
|
||||
mqClient = RegisterRocketProducerMust(address.Strings(), groupName, retry)
|
||||
case "kafka":
|
||||
address, _ := g.Cfg().Get(ctx, "queue.kafka.address", nil)
|
||||
if len(address.Strings()) == 0 {
|
||||
panic("queue kafka address is not support")
|
||||
}
|
||||
version, _ := g.Cfg().Get(ctx, "queue.kafka.version", "2.0.0")
|
||||
mqClient = RegisterKafkaProducerMust(KafkaConfig{
|
||||
Brokers: address.Strings(),
|
||||
GroupID: groupName,
|
||||
Version: version.String(),
|
||||
})
|
||||
case "redis":
|
||||
address, _ := g.Cfg().Get(ctx, "queue.redis.address", nil)
|
||||
if len(address.String()) == 0 {
|
||||
panic("queue redis address is not support")
|
||||
}
|
||||
db, _ := g.Cfg().Get(ctx, "queue.redis.db", 0)
|
||||
pass, _ := g.Cfg().Get(ctx, "queue.redis.pass", "")
|
||||
timeout, _ := g.Cfg().Get(ctx, "queue.redis.timeout", 0)
|
||||
|
||||
mqClient = RegisterRedisMqProducerMust(RedisOption{
|
||||
Addr: address.String(),
|
||||
Passwd: pass.String(),
|
||||
DBnum: db.Int(),
|
||||
Timeout: timeout.Int(),
|
||||
}, PoolOption{
|
||||
5, 50, 5,
|
||||
}, groupName, retry)
|
||||
|
||||
default:
|
||||
panic("queue driver is not support")
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
mqProducerInstanceMap[groupName] = mqClient
|
||||
|
||||
return mqClient, nil
|
||||
}
|
||||
|
||||
//
|
||||
// @Title 新建一个消费者实例
|
||||
// @Description
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @Param groupName
|
||||
// @Return mqClient
|
||||
// @Return err
|
||||
//
|
||||
func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
|
||||
// 是否支持创建多个消费者
|
||||
multiComsumer, _ := g.Cfg().Get(ctx, "queue.multiComsumer", true)
|
||||
randTag := string(utils.Charset.RandomCreateBytes(6))
|
||||
if multiComsumer.Bool() == false {
|
||||
randTag = "001"
|
||||
}
|
||||
|
||||
if item, ok := mqConsumerInstanceMap[groupName+"-"+randTag]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
driver, _ := g.Cfg().Get(ctx, "queue.driver", "")
|
||||
|
||||
if groupName == "" {
|
||||
return mqClient, gerror.New("mq groupName is empty.")
|
||||
}
|
||||
|
||||
switch driver.String() {
|
||||
case "rocketmq":
|
||||
address, _ := g.Cfg().Get(ctx, "queue.rocketmq.address", nil)
|
||||
if address == nil {
|
||||
return nil, gerror.New("queue.rocketmq.address is empty.")
|
||||
}
|
||||
|
||||
mqClient = RegisterRocketConsumerMust(address.Strings(), groupName)
|
||||
case "kafka":
|
||||
address, _ := g.Cfg().Get(ctx, "queue.kafka.address", nil)
|
||||
if len(address.Strings()) == 0 {
|
||||
panic("queue kafka address is not support")
|
||||
}
|
||||
version, _ := g.Cfg().Get(ctx, "queue.kafka.version", "2.0.0")
|
||||
|
||||
clientId := "HOTGO-Consumer-" + groupName
|
||||
randClient, _ := g.Cfg().Get(ctx, "queue.kafka.randClient", true)
|
||||
if randClient.Bool() {
|
||||
clientId += "-" + randTag
|
||||
}
|
||||
|
||||
mqClient = RegisterKafkaMqConsumerMust(KafkaConfig{
|
||||
Brokers: address.Strings(),
|
||||
GroupID: groupName,
|
||||
Version: version.String(),
|
||||
ClientId: clientId,
|
||||
})
|
||||
case "redis":
|
||||
address, _ := g.Cfg().Get(ctx, "queue.redis.address", nil)
|
||||
if len(address.String()) == 0 {
|
||||
panic("queue redis address is not support")
|
||||
}
|
||||
db, _ := g.Cfg().Get(ctx, "queue.redis.db", 0)
|
||||
pass, _ := g.Cfg().Get(ctx, "queue.redis.pass", "")
|
||||
timeout, _ := g.Cfg().Get(ctx, "queue.redis.pass", 0)
|
||||
|
||||
mqClient = RegisterRedisMqConsumerMust(RedisOption{
|
||||
Addr: address.String(),
|
||||
Passwd: pass.String(),
|
||||
DBnum: db.Int(),
|
||||
Timeout: timeout.Int(),
|
||||
}, PoolOption{
|
||||
5, 50, 5,
|
||||
}, groupName)
|
||||
default:
|
||||
panic("queue driver is not support")
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
mqConsumerInstanceMap[groupName] = mqClient
|
||||
|
||||
return mqClient, nil
|
||||
}
|
||||
|
||||
//
|
||||
// @Title 返回消息体
|
||||
// @Description
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @Return string
|
||||
//
|
||||
func (m *MqMsg) BodyString() string {
|
||||
return string(m.Body)
|
||||
}
|
||||
133
hotgo-server/app/factory/queue/queue_test.go
Normal file
133
hotgo-server/app/factory/queue/queue_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRPushQueue(t *testing.T) {
|
||||
|
||||
ll := NewQueue()
|
||||
|
||||
ll.RPush("1")
|
||||
ll.RPush("2")
|
||||
ll.RPush("3")
|
||||
|
||||
go func() {
|
||||
ll.RPush("4")
|
||||
}()
|
||||
go func() {
|
||||
ll.RPush("5")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ll.RPush("6")
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if ll.Len() != 6 {
|
||||
t.Error("list Len() do error #1")
|
||||
}
|
||||
|
||||
listVal := fmt.Sprintf("num=>%v,%v,%v", ll.LPop(), ll.LPop(), ll.LPop())
|
||||
if listVal != "num=>1,2,3" {
|
||||
t.Error("list do error #2")
|
||||
}
|
||||
|
||||
if ll.Len() != 3 {
|
||||
t.Error("list Len() do error #3")
|
||||
}
|
||||
|
||||
ll.LPop()
|
||||
ll.LPop()
|
||||
ll.LPop()
|
||||
c := ll.LPop()
|
||||
|
||||
if c != nil {
|
||||
t.Error("list LPop() do error #4")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestLPushQueue(t *testing.T) {
|
||||
|
||||
ll := NewQueue()
|
||||
|
||||
ll.LPush("1")
|
||||
ll.LPush("2")
|
||||
ll.LPush("3")
|
||||
|
||||
go func() {
|
||||
ll.LPush("4")
|
||||
}()
|
||||
go func() {
|
||||
ll.LPush("5")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ll.LPush("6")
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if ll.Len() != 6 {
|
||||
t.Error("list Len() do error #1")
|
||||
}
|
||||
|
||||
listVal := fmt.Sprintf("num=>%v,%v,%v", ll.RPop(), ll.RPop(), ll.RPop())
|
||||
if listVal != "num=>1,2,3" {
|
||||
t.Error("list do error #2")
|
||||
}
|
||||
|
||||
if ll.Len() != 3 {
|
||||
t.Error("list Len() do error #3")
|
||||
}
|
||||
|
||||
ll.RPop()
|
||||
ll.RPop()
|
||||
ll.RPop()
|
||||
c := ll.RPop()
|
||||
|
||||
if c != nil {
|
||||
t.Error("list RPop() do error #4")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestRegisterRocketMqProducer(t *testing.T) {
|
||||
ins, err := RegisterRocketMqProducer([]string{}, "tests", 2)
|
||||
if err == nil {
|
||||
t.Error("RegisterRocketMqProducer err #1")
|
||||
}
|
||||
|
||||
ins, err = RegisterRocketMqProducer([]string{"192.168.1.1:9876"}, "tests", 2)
|
||||
if err != nil {
|
||||
t.Error("RegisterRocketMqProducer err #2")
|
||||
}
|
||||
|
||||
if ins.endPoints[0] != "192.168.1.1:9876" {
|
||||
t.Error("RegisterRocketMqProducer err #3")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRegisterRocketMqConsumer(t *testing.T) {
|
||||
ins, err := RegisterRocketMqConsumer([]string{}, "tests")
|
||||
if err == nil {
|
||||
t.Error("RegisterRocketMqConsumer err #1")
|
||||
}
|
||||
|
||||
ins, err = RegisterRocketMqProducer([]string{"192.168.1.1:9876"}, "tests", 2)
|
||||
if err != nil {
|
||||
t.Error("RegisterRocketMqConsumer err #2")
|
||||
}
|
||||
|
||||
if ins.endPoints[0] != "192.168.1.1:9876" {
|
||||
t.Error("RegisterRocketMqConsumer err #3")
|
||||
}
|
||||
|
||||
}
|
||||
284
hotgo-server/app/factory/queue/redismq.go
Normal file
284
hotgo-server/app/factory/queue/redismq.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bufanyun/hotgo/app/consts"
|
||||
"github.com/bufanyun/hotgo/app/utils"
|
||||
"github.com/bufanyun/pool"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedisMq struct {
|
||||
poolName string
|
||||
groupName string
|
||||
retry int
|
||||
timeout int
|
||||
}
|
||||
|
||||
type PoolOption struct {
|
||||
InitCap int
|
||||
MaxCap int
|
||||
IdleTimeout int
|
||||
}
|
||||
|
||||
type RedisOption struct {
|
||||
Addr string
|
||||
Passwd string
|
||||
DBnum int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
var redisPoolMap map[string]pool.Pool
|
||||
|
||||
func init() {
|
||||
redisPoolMap = make(map[string]pool.Pool)
|
||||
|
||||
}
|
||||
|
||||
// SendMsg 按字符串类型生产数据
|
||||
func (r *RedisMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
|
||||
return r.SendByteMsg(topic, []byte(body))
|
||||
}
|
||||
|
||||
// SendByteMsg 生产数据
|
||||
func (r *RedisMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error) {
|
||||
if r.poolName == "" {
|
||||
return mqMsg, gerror.New("RedisMq producer not register")
|
||||
}
|
||||
if topic == "" {
|
||||
return mqMsg, gerror.New("RedisMq topic is empty")
|
||||
}
|
||||
|
||||
msgId := getRandMsgId()
|
||||
rdx, put, err := getRedis(r.poolName, r.retry)
|
||||
defer put()
|
||||
|
||||
if err != nil {
|
||||
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者获取redis实例失败:", err))
|
||||
}
|
||||
|
||||
mqMsg = MqMsg{
|
||||
RunType: SendMsg,
|
||||
Topic: topic,
|
||||
MsgId: msgId,
|
||||
Body: body,
|
||||
}
|
||||
mqMsgJson, err := json.Marshal(mqMsg)
|
||||
if err != nil {
|
||||
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者解析json消息失败:", err))
|
||||
}
|
||||
|
||||
queueName := r.genQueueName(r.groupName, topic)
|
||||
|
||||
_, err = redis.Int64(rdx.Do("LPUSH", queueName, mqMsgJson))
|
||||
if err != nil {
|
||||
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者添加消息失败:", err))
|
||||
}
|
||||
|
||||
if r.timeout > 0 {
|
||||
_, err = rdx.Do("EXPIRE", queueName, r.timeout)
|
||||
if err != nil {
|
||||
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者设置过期时间失败:", err))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListenReceiveMsgDo 消费数据
|
||||
func (r *RedisMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
|
||||
if r.poolName == "" {
|
||||
return gerror.New("RedisMq producer not register")
|
||||
}
|
||||
if topic == "" {
|
||||
return gerror.New("RedisMq topic is empty")
|
||||
}
|
||||
|
||||
queueName := r.genQueueName(r.groupName, topic)
|
||||
|
||||
go func() {
|
||||
for range time.Tick(1000 * time.Millisecond) {
|
||||
mqMsgList := r.loopReadQueue(queueName)
|
||||
for _, mqMsg := range mqMsgList {
|
||||
receiveDo(mqMsg)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 生成队列名称
|
||||
func (r *RedisMq) genQueueName(groupName string, topic string) string {
|
||||
return fmt.Sprintf(consts.QueueName+"%s-%s", groupName, topic)
|
||||
}
|
||||
|
||||
func (r *RedisMq) loopReadQueue(queueName string) (mqMsgList []MqMsg) {
|
||||
rdx, put, err := getRedis(r.poolName, r.retry)
|
||||
defer put()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
infoByte, err := redis.Bytes(rdx.Do("RPOP", queueName))
|
||||
if err != nil || len(infoByte) == 0 {
|
||||
break
|
||||
}
|
||||
var mqMsg MqMsg
|
||||
json.Unmarshal(infoByte, &mqMsg)
|
||||
if mqMsg.MsgId != "" {
|
||||
mqMsgList = append(mqMsgList, mqMsg)
|
||||
}
|
||||
|
||||
}
|
||||
return mqMsgList
|
||||
}
|
||||
|
||||
func RegisterRedisMqProducerMust(connOpt RedisOption, poolOpt PoolOption, groupName string, retry int) (client MqProducer) {
|
||||
var err error
|
||||
client, err = RegisterRedisMq(connOpt, poolOpt, groupName, retry)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// RegisterRedisMqConsumerMust 注册消费者
|
||||
func RegisterRedisMqConsumerMust(connOpt RedisOption, poolOpt PoolOption, groupName string) (client MqConsumer) {
|
||||
var err error
|
||||
client, err = RegisterRedisMq(connOpt, poolOpt, groupName, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// RegisterRedisMq 注册redismq实例
|
||||
func RegisterRedisMq(connOpt RedisOption, poolOpt PoolOption, groupName string, retry int) (mqIns *RedisMq, err error) {
|
||||
poolName, err := registerRedis(connOpt.Addr, connOpt.Passwd, connOpt.DBnum, poolOpt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if retry <= 0 {
|
||||
retry = 0
|
||||
}
|
||||
|
||||
mqIns = &RedisMq{
|
||||
poolName: poolName,
|
||||
groupName: groupName,
|
||||
retry: retry,
|
||||
timeout: connOpt.Timeout,
|
||||
}
|
||||
|
||||
return mqIns, nil
|
||||
}
|
||||
|
||||
// RegisterRedis 注册一个redis配置
|
||||
func registerRedis(host, pass string, dbnum int, opt PoolOption) (poolName string, err error) {
|
||||
poolName = utils.Charset.Md5ToString(fmt.Sprintf("%s-%s-%d", host, pass, dbnum))
|
||||
if _, ok := redisPoolMap[poolName]; ok {
|
||||
return poolName, nil
|
||||
}
|
||||
|
||||
connRedis := func() (interface{}, error) {
|
||||
conn, err := redis.Dial("tcp", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pass != "" {
|
||||
_, err := conn.Do("AUTH", pass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if dbnum > 0 {
|
||||
_, err := conn.Do("SELECT", dbnum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// closeRedis 关闭连接
|
||||
closeRedis := func(v interface{}) error {
|
||||
return v.(redis.Conn).Close()
|
||||
}
|
||||
|
||||
// pingRedis 检测连接连通性
|
||||
pingRedis := func(v interface{}) error {
|
||||
conn := v.(redis.Conn)
|
||||
|
||||
val, err := redis.String(conn.Do("PING"))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val != "PONG" {
|
||||
return gerror.New("queue redis ping is error ping => " + val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p, err := pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: opt.InitCap,
|
||||
MaxCap: opt.MaxCap,
|
||||
Factory: connRedis,
|
||||
Close: closeRedis,
|
||||
Ping: pingRedis,
|
||||
IdleTimeout: time.Duration(opt.IdleTimeout) * time.Second,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return poolName, err
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
redisPoolMap[poolName] = p
|
||||
|
||||
return poolName, nil
|
||||
}
|
||||
|
||||
// getRedis 获取一个redis db连接
|
||||
func getRedis(poolName string, retry int) (db redis.Conn, put func(), err error) {
|
||||
put = func() {}
|
||||
if _, ok := redisPoolMap[poolName]; ok == false {
|
||||
return nil, put, gerror.New("db connect is nil")
|
||||
}
|
||||
redisPool := redisPoolMap[poolName]
|
||||
|
||||
conn, err := redisPool.Get()
|
||||
for i := 0; i < retry; i++ {
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
conn, err = redisPool.Get()
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, put, err
|
||||
}
|
||||
put = func() {
|
||||
redisPool.Put(conn)
|
||||
}
|
||||
|
||||
db = conn.(redis.Conn)
|
||||
return db, put, nil
|
||||
}
|
||||
|
||||
func getRandMsgId() (msgId string) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
radium := rand.Intn(999) + 1
|
||||
timeCode := time.Now().UnixNano()
|
||||
|
||||
msgId = fmt.Sprintf("%d%.4d", timeCode, radium)
|
||||
return msgId
|
||||
}
|
||||
165
hotgo-server/app/factory/queue/rocketmq.go
Normal file
165
hotgo-server/app/factory/queue/rocketmq.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/apache/rocketmq-client-go/v2"
|
||||
"github.com/apache/rocketmq-client-go/v2/consumer"
|
||||
"github.com/apache/rocketmq-client-go/v2/primitive"
|
||||
"github.com/apache/rocketmq-client-go/v2/producer"
|
||||
"github.com/apache/rocketmq-client-go/v2/rlog"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type RocketMq struct {
|
||||
endPoints []string
|
||||
producerIns rocketmq.Producer
|
||||
consumerIns rocketmq.PushConsumer
|
||||
}
|
||||
|
||||
// 重写日志
|
||||
func rewriteLog() {
|
||||
level, _ := g.Cfg().Get(ctx, "queue.rocketmq.logLevel", "debug")
|
||||
rlog.SetLogger(&RocketMqLogger{Flag: "[rocket_mq]", LevelLog: level.String()})
|
||||
}
|
||||
|
||||
// RegisterRocketProducerMust 注册并启动生产者接口实现
|
||||
func RegisterRocketProducerMust(endPoints []string, groupName string, retry int) (client MqProducer) {
|
||||
rewriteLog()
|
||||
var err error
|
||||
client, err = RegisterRocketMqProducer(endPoints, groupName, retry)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// RegisterRocketConsumerMust 注册消费者
|
||||
func RegisterRocketConsumerMust(endPoints []string, groupName string) (client MqConsumer) {
|
||||
rewriteLog()
|
||||
var err error
|
||||
client, err = RegisterRocketMqConsumer(endPoints, groupName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// SendMsg 按字符串类型生产数据
|
||||
func (r *RocketMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
|
||||
return r.SendByteMsg(topic, []byte(body))
|
||||
}
|
||||
|
||||
// SendByteMsg 生产数据
|
||||
func (r *RocketMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error) {
|
||||
if r.producerIns == nil {
|
||||
return mqMsg, errors.New("RocketMq producer not register")
|
||||
}
|
||||
|
||||
result, err := r.producerIns.SendSync(context.Background(), &primitive.Message{
|
||||
Topic: topic,
|
||||
Body: body,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.Status != primitive.SendOK {
|
||||
return mqMsg, errors.New(fmt.Sprintf("RocketMq producer send msg error status:%v", result.Status))
|
||||
}
|
||||
|
||||
mqMsg = MqMsg{
|
||||
RunType: SendMsg,
|
||||
Topic: topic,
|
||||
MsgId: result.MsgID,
|
||||
Body: body,
|
||||
}
|
||||
return mqMsg, nil
|
||||
}
|
||||
|
||||
// ListenReceiveMsgDo 消费数据
|
||||
func (r *RocketMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
|
||||
if r.consumerIns == nil {
|
||||
return errors.New("RocketMq consumer not register")
|
||||
}
|
||||
|
||||
err = r.consumerIns.Subscribe(topic, consumer.MessageSelector{}, func(ctx context.Context,
|
||||
msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
|
||||
for _, item := range msgs {
|
||||
go receiveDo(MqMsg{
|
||||
RunType: ReceiveMsg,
|
||||
Topic: item.Topic,
|
||||
MsgId: item.MsgId,
|
||||
Body: item.Body,
|
||||
})
|
||||
}
|
||||
return consumer.ConsumeSuccess, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.consumerIns.Start()
|
||||
if err != nil {
|
||||
r.consumerIns.Unsubscribe(topic)
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RegisterRocketMqProducer 注册rocketmq生产者
|
||||
func RegisterRocketMqProducer(endPoints []string, groupName string, retry int) (mqIns *RocketMq, err error) {
|
||||
addr, err := primitive.NewNamesrvAddr(endPoints...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mqIns = &RocketMq{
|
||||
endPoints: endPoints,
|
||||
}
|
||||
|
||||
if retry <= 0 {
|
||||
retry = 0
|
||||
}
|
||||
|
||||
mqIns.producerIns, err = rocketmq.NewProducer(
|
||||
producer.WithNameServer(addr),
|
||||
producer.WithRetry(retry),
|
||||
producer.WithGroupName(groupName),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = mqIns.producerIns.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mqIns, nil
|
||||
}
|
||||
|
||||
// RegisterRocketMqConsumer 注册rocketmq消费者
|
||||
func RegisterRocketMqConsumer(endPoints []string, groupName string) (mqIns *RocketMq, err error) {
|
||||
addr, err := primitive.NewNamesrvAddr(endPoints...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mqIns = &RocketMq{
|
||||
endPoints: endPoints,
|
||||
}
|
||||
mqIns.consumerIns, err = rocketmq.NewPushConsumer(
|
||||
consumer.WithNameServer(addr),
|
||||
consumer.WithConsumerModel(consumer.Clustering),
|
||||
consumer.WithGroupName(groupName),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mqIns, nil
|
||||
}
|
||||
83
hotgo-server/app/factory/queue/rocketmq_rewrite_logger.go
Normal file
83
hotgo-server/app/factory/queue/rocketmq_rewrite_logger.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type RocketMqLogger struct {
|
||||
Flag string
|
||||
LevelLog string
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) Debug(msg string, fields map[string]interface{}) {
|
||||
if l.LevelLog == "close" {
|
||||
return
|
||||
}
|
||||
if msg == "" && len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if l.LevelLog == "debug" || l.LevelLog == "all" {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [debug] ", msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) Level(level string) {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [level] ", level))
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) OutputPath(path string) (err error) {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [path] ", path))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) Info(msg string, fields map[string]interface{}) {
|
||||
if l.LevelLog == "close" {
|
||||
return
|
||||
}
|
||||
if msg == "" && len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if l.LevelLog == "info" || l.LevelLog == "all" {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [info] ", msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) Warning(msg string, fields map[string]interface{}) {
|
||||
if l.LevelLog == "close" {
|
||||
return
|
||||
}
|
||||
if msg == "" && len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if l.LevelLog == "warn" || l.LevelLog == "all" {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [warn] ", msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) Error(msg string, fields map[string]interface{}) {
|
||||
if l.LevelLog == "close" {
|
||||
return
|
||||
}
|
||||
if msg == "" && len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
if l.LevelLog == "error" || l.LevelLog == "all" {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [error] ", msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RocketMqLogger) Fatal(msg string, fields map[string]interface{}) {
|
||||
if l.LevelLog == "close" {
|
||||
return
|
||||
}
|
||||
if msg == "" && len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if l.LevelLog == "fatal" || l.LevelLog == "all" {
|
||||
Log(ctx, fmt.Sprint(l.Flag, " [fatal] ", msg))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user