今日已办
v1
验证 StageHandler 在处理消息时是否为单例,【错误尝试】
go
type StageHandler struct {
}
func (s StageHandler) Middleware1(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
log.Logger.Info("StageHandler Middleware 1")
fmt.Printf("%p\n", &s)
return h(msg)
}
}
func (s StageHandler) Middleware2(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
log.Logger.Info("StageHandler Middleware 2")
fmt.Printf("%p\n", &s)
return h(msg)
}
}
func (s StageHandler) Middleware3(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
log.Logger.Info("StageHandler Middleware 3")
fmt.Printf("%p\n", &s)
return h(msg)
}
}
func (s StageHandler) Handler1(msg *message.Message) error {
log.Logger.Info("StageHandler Handler 1")
fmt.Printf("%p\n", &s)
return nil
}
v2
- 定义不同 Handler
go
type CrashHandler struct {
Topic string
}
func (s CrashHandler) Handler1(msg *message.Message) error {
log.Logger.Info(s.Topic + ": CrashHandler Handler 1 start")
fmt.Printf("%p\n", &s)
time.Sleep(1 * time.Second)
log.Logger.Info(s.Topic + ": CrashHandler Handler 1 end")
return nil
}
type LagHandler struct {
Topic string
}
func (s LagHandler) Handler1(msg *message.Message) error {
log.Logger.Info(s.Topic + ": LagHandler Handler 1 start")
fmt.Printf("%p\n", &s)
time.Sleep(1 * time.Second)
log.Logger.Info(s.Topic + ": LagHandler Handler 1 end")
return nil
}
- 添加到router中
go
for _, topic := range topics {
var category string
var handlerFunc message.NoPublishHandlerFunc
if strings.Contains(topic, performance.CategoryCrash) {
category = performance.CategoryCrash
handlerFunc = CrashHandler{Topic: category}.Handler1
} else if strings.Contains(topic, performance.CategoryLag) {
category = performance.CategoryLag
handlerFunc = LagHandler{Topic: category}.Handler1
} else {
continue
}
handler := router.AddNoPublisherHandler(topic+"test-handler", topic, subscriber, handlerFunc)
}
- 结论
- handler 实例会不断创建
- 不同的 handler 可以并行处理不同主题的消息
- 相同的 handler 在处理该主题的消息时是顺序的
官方文档: Message Router (watermill.io)
订阅者可以一次消费一条消息,也可以并行消费多条消息
- Single stream of messages 是最简单的方法,这意味着在调用
msg.Ack()
之前,订阅者将不会收到任何新消息 - Multiple message streams 仅部分订阅者支持。通过一次订阅多个主题分区 ,可以并行消费多条消息,甚至是之前未确认的消息(例如,Kafka 订阅者就是这样工作的) Router 通过运行并发 HandlerFuncs(每个分区一个)来处理此模型
v3
存在并发安全问题
- 公用一个上下文
- 频繁的修改上下文中的字段值
- 不同Handler和MiddleWare存在并发
解决思路
- 将一次消息处理会使用到的数据集合定义为一个结构体
go
type ContextData struct {
Status int
Event schema.Event
AppID string // API 上报
FetchScenario string // API 上报
}
- 使用message的Context来传递这个数据
- 移除掉 ProfileCtx 的相关设计
- 使用
watermillzap.Logger
来替换本身的LoggerAdapter
,更加直观且与原项目适配
完整代码
profile/internal/watermill/consumer/consumer_context.go
go
// Package consumer
// @Author xzx 2023/8/11 18:53:00
package consumer
import (
"context"
kc "github.com/Kevinello/kafka-client"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
"github.com/ThreeDotsLabs/watermill/message/router/plugin"
"github.com/garsue/watermillzap"
"github.com/qiulin/watermill-kafkago/pkg/kafkago"
"go.uber.org/zap"
"profile/internal/config"
"profile/internal/connector"
"profile/internal/log"
"profile/internal/schema/performance"
"strings"
"time"
)
// Consume
// @Description
// @Author xzx 2023-08-16 22:52:52
func Consume() {
logger := watermillzap.NewLogger(log.Logger)
publisher, subscriber := newPubSub(logger)
router, err := message.NewRouter(message.RouterConfig{}, logger)
if err != nil {
log.Logger.Fatal("creates a new Router with given configuration error", zap.Error(err))
}
router.AddPlugin(plugin.SignalsHandler)
router.AddMiddleware(
middleware.Retry{
MaxRetries: 3,
InitialInterval: time.Millisecond * 100,
Logger: logger,
}.Middleware,
middleware.Recoverer,
)
getTopics := kc.GetTopicReMatch(strings.Split(config.Profile.GetString("kafka.topicRE"), ","))
topics, err := getTopics(config.Profile.GetString("kafka.bootstrap"))
if err != nil {
log.Logger.Fatal("get topics failed", zap.Error(err))
return
}
for _, topic := range topics {
var category string
var handlerFunc message.HandlerFunc
if strings.Contains(topic, performance.CategoryCrash) {
category = performance.CategoryCrash
handlerFunc = CrashWriteKafka
} else if strings.Contains(topic, performance.CategoryLag) {
category = performance.CategoryLag
handlerFunc = LagWriteKafka
} else {
continue
}
router.AddHandler(category, topic, subscriber, connector.GetTopic(category), publisher, handlerFunc).
AddMiddleware(
UnpackKafkaMessage,
InitPerformanceEvent,
AnalyzeEvent)
}
if err = router.Run(context.Background()); err != nil {
log.Logger.Error("runs all plugins and handlers and starts subscribing to provided topics error", zap.Error(err))
}
}
// newPubSub
// @Description
// @Author xzx 2023-08-16 22:52:45
// @Param logger
// @Return message.Publisher
// @Return message.Subscriber
func newPubSub(logger watermill.LoggerAdapter) (message.Publisher, message.Subscriber) {
marshaler := kafkago.DefaultMarshaler{}
publisher := kafkago.NewPublisher(kafkago.PublisherConfig{
Brokers: []string{config.Profile.GetString("kafka.bootstrap")},
Async: false,
Marshaler: marshaler,
OTELEnabled: false,
Ipv4Only: true,
Timeout: 100 * time.Second,
}, logger)
subscriber, err := kafkago.NewSubscriber(kafkago.SubscriberConfig{
Brokers: []string{config.Profile.GetString("kafka.bootstrap")},
Unmarshaler: marshaler,
ConsumerGroup: config.Profile.GetString("kafka.group"),
OTELEnabled: false,
}, logger)
if err != nil {
log.Logger.Fatal("Unable to create subscriber", zap.Error(err))
}
return publisher, subscriber
}
profile/internal/watermill/consumer/consumer_stage.go
go
// Package consumer
// @Author xzx 2023/8/12 10:01:00
package consumer
import (
"context"
"encoding/json"
"github.com/ThreeDotsLabs/watermill/message"
"go.uber.org/zap"
"profile/internal/connector"
"profile/internal/log"
"profile/internal/schema"
"profile/internal/schema/performance"
"profile/internal/state"
)
type ContextData struct {
Status int
Event schema.Event
AppID string // API 上报
FetchScenario string // API 上报
}
// UnpackKafkaMessage
// @Description
// @Author xzx 2023-08-12 12:27:30
// @Param h
// @Return message.HandlerFunc
func UnpackKafkaMessage(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
var data ContextData
// 反序列化,存入通用结构体
if contextErr := json.Unmarshal(msg.Payload, &data.Event); contextErr != nil {
data.Status = state.StatusUnmarshalError
return nil, contextErr
}
log.Logger.Info("[1-UnpackKafkaItem] unpack kafka item success", zap.Any("event", data.Event))
msg.SetContext(context.WithValue(msg.Context(), "data", data))
return h(msg)
}
}
// InitPerformanceEvent
// @Description
// @Author xzx 2023-08-12 12:27:35
// @Param h
// @Return message.HandlerFunc
func InitPerformanceEvent(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
data := msg.Context().Value("data").(ContextData)
event, contextErr := performance.EventFactory(data.Event.Category, data.Event.Dimensions, data.Event.Values)
if contextErr != nil {
data.Status = state.StatusEventFactoryError
return nil, contextErr
}
log.Logger.Info("[2-InitPerformanceEvent] Consume performance event success", zap.Any("event", data.Event))
data.Event.ProfileData = event
msg.SetContext(context.WithValue(msg.Context(), "data", data))
return h(msg)
}
}
// AnalyzeEvent
// @Description
// @Author xzx 2023-08-12 12:27:38
// @Param h
// @Return message.HandlerFunc
func AnalyzeEvent(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
data := msg.Context().Value("data").(ContextData)
contextErr := data.Event.ProfileData.Analyze()
if contextErr != nil {
data.Status = state.StatusAnalyzeError
return nil, contextErr
}
log.Logger.Info("[3-AnalyzeEvent] analyze event success", zap.Any("event", data.Event))
// clear dimensions and values
data.Event.Dimensions = nil
data.Event.Values = nil
msg.SetContext(context.WithValue(msg.Context(), "data", data))
return h(msg)
}
}
// CrashWriteKafka
// @Description
// @Author xzx 2023-08-12 15:09:15
// @Param msg
// @Return []*message.Message
// @Return error
func CrashWriteKafka(msg *message.Message) ([]*message.Message, error) {
data := msg.Context().Value("data").(ContextData)
toWriteBytes, contextErr := json.Marshal(data.Event)
if contextErr != nil {
data.Status = state.StatusUnmarshalError
return nil, contextErr
}
msg = message.NewMessage(data.Event.ID, toWriteBytes)
log.Logger.Info("[4-CrashWriteKafka] write kafka success", zap.String("topic", connector.GetTopic(data.Event.Category)), zap.String("id", data.Event.ID), zap.String("msg", string(toWriteBytes)))
return message.Messages{msg}, nil
}
func LagWriteKafka(msg *message.Message) ([]*message.Message, error) {
data := msg.Context().Value("data").(ContextData)
toWriteBytes, contextErr := json.Marshal(data.Event)
if contextErr != nil {
data.Status = state.StatusUnmarshalError
return nil, contextErr
}
msg = message.NewMessage(data.Event.ID, toWriteBytes)
log.Logger.Info("[4-LagWriteKafka] write kafka success", zap.String("topic", connector.GetTopic(data.Event.Category)), zap.String("id", data.Event.ID), zap.String("msg", string(toWriteBytes)))
return message.Messages{msg}, nil
}
测试
上报PERF_LAG Event
可以并发处理 2 条消息,不必等待上一条消息处理完
多次测试发现是由于两条消息走了不同的 Handler
暂未修复,明明是同一主题的两条消息却都走了两条不同的链路,而且 publisher 最后写回的主题也是写到了不同的主题上,并且上报另一个类型的事件,即另一个主题的消息却无法触发消费者消费!
暂定先写死两个主题名称测试是否正常
明日待办
- 开会讨论项目规划和任务分工
- 继续完成需求