16 changed files with 614 additions and 103 deletions
@ -0,0 +1,383 @@ |
|||||||
|
package mq |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"runtime/debug" |
||||||
|
"sig-pub/pkg/zlog" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/bytedance/sonic" |
||||||
|
"github.com/gogo/protobuf/proto" |
||||||
|
"github.com/nats-io/nats.go" |
||||||
|
"github.com/nats-io/nats.go/jetstream" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
NatsConn *nats.Conn |
||||||
|
NatsJS jetstream.JetStream |
||||||
|
) |
||||||
|
|
||||||
|
func InitNats(natsUrl string) (err error) { |
||||||
|
options := nats.Options{ |
||||||
|
Url: natsUrl, |
||||||
|
// 断线重连配置
|
||||||
|
RetryOnFailedConnect: true, |
||||||
|
AllowReconnect: true, |
||||||
|
MaxReconnect: -1, |
||||||
|
ReconnectWait: time.Second * 2, |
||||||
|
ReconnectBufSize: 10 * 1024 * 1024, // 10M
|
||||||
|
} |
||||||
|
|
||||||
|
NatsConn, err = options.Connect() |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
NatsJS, err = jetstream.New(NatsConn) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) |
||||||
|
defer cancel() |
||||||
|
err = initNatsStream(ctx) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// initNatsStream 初始化 jetstream 消息流
|
||||||
|
func initNatsStream(ctx context.Context) (err error) { |
||||||
|
for _, ss := range initialStream { |
||||||
|
if len(ss) < 2 { |
||||||
|
err = fmt.Errorf("initial jetstream %v config error", ss) |
||||||
|
return |
||||||
|
} |
||||||
|
err = NatsCreateStream(ctx, ss[0], ss[1:]...) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// NatsCreateStream 创建或更新jetstream消息流
|
||||||
|
func NatsCreateStream(ctx context.Context, streamName string, subjects ...string) (err error) { |
||||||
|
// 活动消息流 https://docs.nats.io/nats-concepts/jetstream/streams#configuration
|
||||||
|
streamConfig := jetstream.StreamConfig{ |
||||||
|
Name: streamName, |
||||||
|
Storage: jetstream.FileStorage, |
||||||
|
Subjects: subjects, // 要绑定的topics
|
||||||
|
Replicas: 1, // 消息副本数量
|
||||||
|
MaxAge: time.Hour * 24, // 流中任何消息的最大年龄
|
||||||
|
MaxBytes: -1, // 存储的所有消息最大字节数 -1无限制
|
||||||
|
MaxMsgs: -1, // 流中存储的所有消息最大消息数
|
||||||
|
MaxMsgSize: -1, // 单个消息最大字节数
|
||||||
|
MaxConsumers: -1, // Stream 定义的最大消费者数量
|
||||||
|
NoAck: false, |
||||||
|
Retention: jetstream.LimitsPolicy, // 声明流的保留策略
|
||||||
|
Discard: jetstream.DiscardOld, // stream消息数量达到limit处理策略
|
||||||
|
MaxMsgsPerSubject: -1, // stream保留每个主题最大消息数
|
||||||
|
Duplicates: time.Minute * 2, // 跟踪重复消息的窗口
|
||||||
|
AllowRollup: false, // 允许使用标题Nats-Rollup将流的所有内容或流中的主题替换为单个新消息
|
||||||
|
DenyDelete: true, // 不允许删除消息
|
||||||
|
DenyPurge: true, // 不允许清空消息
|
||||||
|
} |
||||||
|
_, err = NatsJS.CreateOrUpdateStream(ctx, streamConfig) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// NatsCreateConsumer 创建或更新 jetstream consumer
|
||||||
|
// consumerName 多个相同name只消费一次,多个不同name消费分别消费一次
|
||||||
|
func NatsCreateConsumer[T any](consumerName, streamName, topic string, inst func() T, handler func(T) (err error), options ...NatsConsumerOption) { |
||||||
|
if NatsJS == nil { |
||||||
|
panic(errors.New("nats js not connect")) |
||||||
|
} |
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) |
||||||
|
defer cancel() |
||||||
|
|
||||||
|
consumerName = strings.ReplaceAll(fmt.Sprintf("%s-%s", consumerName, topic), ".", "-") |
||||||
|
// https://docs.nats.io/nats-concepts/jetstream/consumers#general
|
||||||
|
consumerConfig := jetstream.ConsumerConfig{ |
||||||
|
Durable: consumerName, |
||||||
|
Name: consumerName, |
||||||
|
FilterSubject: topic, |
||||||
|
DeliverPolicy: jetstream.DeliverNewPolicy, // 只处理consumer创建后的新消息
|
||||||
|
AckPolicy: jetstream.AckExplicitPolicy, // 消息确认:不确认高qps
|
||||||
|
ReplayPolicy: jetstream.ReplayInstantPolicy, // 消息回放机制
|
||||||
|
MaxDeliver: -1, // 消息投递的最大次数-1表示没ack就不停投递
|
||||||
|
MaxAckPending: -1, // 多少条消息未投递成功则不再投递新消息-1不限制
|
||||||
|
} |
||||||
|
for _, opt := range options { |
||||||
|
opt(&consumerConfig) |
||||||
|
} |
||||||
|
consumer, err := NatsJS.Consumer(ctx, streamName, consumerName) |
||||||
|
if err != nil { |
||||||
|
consumer, err = NatsJS.CreateConsumer(ctx, streamName, consumerConfig) |
||||||
|
if err != nil { |
||||||
|
// consumer 配置修改
|
||||||
|
if err == jetstream.ErrConsumerExists { |
||||||
|
consumer, err = NatsJS.UpdateConsumer(ctx, streamName, consumerConfig) |
||||||
|
if err != nil { |
||||||
|
// NatsJS.DeleteConsumer(ctx, streamName, consumerName)
|
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
consumer.Consume(func(msg jetstream.Msg) { |
||||||
|
defer func() { |
||||||
|
if r := recover(); r != nil { |
||||||
|
zlog.Errorf("nats consumer recover error: consumer=%s, stream=%s, topic=%s, %v", consumerName, streamName, topic, r) |
||||||
|
debug.PrintStack() |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
defer msg.Ack() |
||||||
|
|
||||||
|
var data T |
||||||
|
if inst != nil { |
||||||
|
data = inst() |
||||||
|
if err := natsJsUnmarshal(data, msg); err != nil { |
||||||
|
zlog.Errorf("unmarshal data %T error, %s, %v", data, msg.Subject(), err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
err = handler(data) |
||||||
|
if err != nil { |
||||||
|
zlog.Error(err) |
||||||
|
debug.PrintStack() |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func NatsCreateConsumerExtend[T any](consumerName, streamName, topic string, inst func() T, handler func(T, *jetstream.MsgMetadata) (ack bool, delay time.Duration, err error), options ...NatsConsumerOption) { |
||||||
|
if NatsJS == nil { |
||||||
|
panic(errors.New("nats js not connect")) |
||||||
|
} |
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) |
||||||
|
defer cancel() |
||||||
|
|
||||||
|
consumerName = strings.ReplaceAll(fmt.Sprintf("%s-%s", consumerName, topic), ".", "-") |
||||||
|
// https://docs.nats.io/nats-concepts/jetstream/consumers#general
|
||||||
|
consumerConfig := jetstream.ConsumerConfig{ |
||||||
|
Durable: consumerName, |
||||||
|
Name: consumerName, |
||||||
|
FilterSubject: topic, |
||||||
|
DeliverPolicy: jetstream.DeliverNewPolicy, // 只处理consumer创建后的新消息
|
||||||
|
AckPolicy: jetstream.AckExplicitPolicy, // 消息确认:不确认高qps
|
||||||
|
ReplayPolicy: jetstream.ReplayInstantPolicy, // 消息回放机制
|
||||||
|
MaxDeliver: -1, // 消息投递的最大次数-1表示没ack就不停投递
|
||||||
|
MaxAckPending: -1, // 多少条消息未投递成功则不再投递新消息-1不限制
|
||||||
|
} |
||||||
|
for _, opt := range options { |
||||||
|
opt(&consumerConfig) |
||||||
|
} |
||||||
|
consumer, err := NatsJS.Consumer(ctx, streamName, consumerName) |
||||||
|
if err != nil { |
||||||
|
consumer, err = NatsJS.CreateConsumer(ctx, streamName, consumerConfig) |
||||||
|
if err != nil { |
||||||
|
// consumer 配置修改
|
||||||
|
if err == jetstream.ErrConsumerExists { |
||||||
|
consumer, err = NatsJS.UpdateConsumer(ctx, streamName, consumerConfig) |
||||||
|
if err != nil { |
||||||
|
// NatsJS.DeleteConsumer(ctx, streamName, consumerName)
|
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
consumer.Consume(func(msg jetstream.Msg) { |
||||||
|
defer func() { |
||||||
|
if r := recover(); r != nil { |
||||||
|
zlog.Errorf("nats consumer recover error: consumer=%s, stream=%s, topic=%s, %v", consumerName, streamName, topic, r) |
||||||
|
debug.PrintStack() |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
var data T |
||||||
|
if inst != nil { |
||||||
|
data = inst() |
||||||
|
if err := natsJsUnmarshal(data, msg); err != nil { |
||||||
|
zlog.Errorf("unmarshal data %T error, %s, %v", data, msg.Subject(), err) |
||||||
|
msg.Ack() // 解析失败直接确认,避免无限重试
|
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 获取消息元数据
|
||||||
|
metadata, err := msg.Metadata() |
||||||
|
if err != nil { |
||||||
|
zlog.Errorf("get message metadata error: %v", err) |
||||||
|
msg.Ack() // 获取元数据失败直接确认
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// 调用handler并处理结果
|
||||||
|
ack, delay, err := handler(data, metadata) |
||||||
|
if err != nil { |
||||||
|
zlog.Error(err) |
||||||
|
debug.PrintStack() |
||||||
|
} |
||||||
|
|
||||||
|
// 根据返回值决定确认或重试
|
||||||
|
if ack { |
||||||
|
msg.Ack() |
||||||
|
} else { |
||||||
|
if err := msg.NakWithDelay(delay); err != nil { |
||||||
|
zlog.Errorf("nak with delay error: %v", err) |
||||||
|
msg.Nak() // 如果设置延迟失败,直接Nak
|
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// NatsSubscribe nats 请求响应注册
|
||||||
|
func NatsSubscribe[T, V any](subject string, inst func() T, handler func(req T) (rsp V, err error)) (s *nats.Subscription) { |
||||||
|
if NatsConn == nil { |
||||||
|
panic(errors.New("nats not connect")) |
||||||
|
} |
||||||
|
s, err := NatsConn.Subscribe(subject, func(msg *nats.Msg) { |
||||||
|
defer func() { |
||||||
|
if r := recover(); r != nil { |
||||||
|
zlog.Errorf("nats subject recover error: topic=%s, %v", subject, r) |
||||||
|
debug.PrintStack() |
||||||
|
} |
||||||
|
}() |
||||||
|
defer msg.Ack() |
||||||
|
|
||||||
|
var req T |
||||||
|
if inst != nil { |
||||||
|
req = inst() |
||||||
|
if err := natsUnmarshal(req, msg); err != nil { |
||||||
|
zlog.Errorf("unmarshal data %T error, %s", req, msg.Subject) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
rsp, err := handler(req) |
||||||
|
|
||||||
|
rspMsg := nats.NewMsg(subject) |
||||||
|
if err != nil { |
||||||
|
rspMsg.Header.Add("error", err.Error()) |
||||||
|
} else { |
||||||
|
if err := natsMarshal(rsp, rspMsg); err != nil { |
||||||
|
zlog.Errorf("marshal rsp data %T error, %s", rsp, msg.Subject) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
msg.RespondMsg(rspMsg) |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// NatsRequest nats发送请求
|
||||||
|
func NatsRequest(subject string, rsp, req any, options ...NatsRequestOption) (err error) { |
||||||
|
if NatsConn == nil { |
||||||
|
return errors.New("nats not connect") |
||||||
|
} |
||||||
|
c := &NatsRequestOptions{Timeout: time.Second * 10} |
||||||
|
for _, option := range options { |
||||||
|
option(c) |
||||||
|
} |
||||||
|
reqMsg := nats.NewMsg(subject) |
||||||
|
if err = natsMarshal(req, reqMsg); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
rspMsg, err := NatsConn.RequestMsg(reqMsg, c.Timeout) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if e := rspMsg.Header.Get("error"); e != "" { |
||||||
|
err = errors.New(e) |
||||||
|
return |
||||||
|
} |
||||||
|
if err = natsUnmarshal(rsp, rspMsg); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// NatsPublish nats 发布消息
|
||||||
|
func NatsPublish(subject string, data any) (err error) { |
||||||
|
if NatsJS == nil { |
||||||
|
return errors.New("nats js not connect") |
||||||
|
} |
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) |
||||||
|
defer cancel() |
||||||
|
|
||||||
|
msg := nats.NewMsg(subject) |
||||||
|
if err = natsMarshal(data, msg); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
_, err = NatsJS.PublishMsg(ctx, msg) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func natsMarshal(data any, msg *nats.Msg) (err error) { |
||||||
|
mt := "1" // 序列化1json,2protobuf
|
||||||
|
if marshaler, ok := data.(proto.Marshaler); ok { |
||||||
|
if bytes, err := marshaler.Marshal(); err == nil { |
||||||
|
mt = "2" |
||||||
|
msg.Data = bytes |
||||||
|
} |
||||||
|
} |
||||||
|
if mt == "1" { |
||||||
|
msg.Data, err = sonic.Marshal(data) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
msg.Header.Add("mt", mt) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func natsUnmarshal(data any, msg *nats.Msg) (err error) { |
||||||
|
mt := msg.Header.Get("mt") |
||||||
|
if mt == "2" { |
||||||
|
unmarshaler, ok := data.(proto.Unmarshaler) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("%T is not protobuf type", data) |
||||||
|
} |
||||||
|
return unmarshaler.Unmarshal(msg.Data) |
||||||
|
} |
||||||
|
// mt=1 json
|
||||||
|
err = sonic.Unmarshal(msg.Data, data) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func natsJsUnmarshal(data any, msg jetstream.Msg) (err error) { |
||||||
|
mt := msg.Headers().Get("mt") |
||||||
|
if mt == "2" { |
||||||
|
unmarshaler, ok := data.(proto.Unmarshaler) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("%T is not protobuf type", data) |
||||||
|
} |
||||||
|
return unmarshaler.Unmarshal(msg.Data()) |
||||||
|
} |
||||||
|
// mt=1 json
|
||||||
|
err = sonic.Unmarshal(msg.Data(), data) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package mq |
||||||
|
|
||||||
|
import "sig-pub/api/pb" |
||||||
|
|
||||||
|
// 交易产品初始化完成通知
|
||||||
|
type PublishExchangeTradeInstanceInited struct { |
||||||
|
Exchange pb.ExchangeType `json:"exchange"` // 交易所类型
|
||||||
|
InstId string `json:"instId"` // 交易产品id
|
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
package mq |
||||||
|
|
||||||
|
import ( |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/nats-io/nats.go/jetstream" |
||||||
|
) |
||||||
|
|
||||||
|
type NatsConsumerOption func(c *jetstream.ConsumerConfig) |
||||||
|
|
||||||
|
type NatsRequestOption func(c *NatsRequestOptions) |
||||||
|
|
||||||
|
type NatsRequestOptions struct { |
||||||
|
Timeout time.Duration |
||||||
|
} |
||||||
|
|
||||||
|
// WithConsumerNoneAck jetstream 消息不ack高qps
|
||||||
|
func WithConsumerNoneAck() NatsConsumerOption { |
||||||
|
return func(c *jetstream.ConsumerConfig) { |
||||||
|
c.AckPolicy = jetstream.AckNonePolicy |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithConsumerMaxDeliver(maxDeliver int) NatsConsumerOption { |
||||||
|
return func(c *jetstream.ConsumerConfig) { |
||||||
|
c.MaxDeliver = maxDeliver |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithConsumerMaxAckPending(maxAckPending int) NatsConsumerOption { |
||||||
|
return func(c *jetstream.ConsumerConfig) { |
||||||
|
c.MaxAckPending = maxAckPending |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithRequestTimeout(timeout time.Duration) NatsRequestOption { |
||||||
|
return func(c *NatsRequestOptions) { |
||||||
|
c.Timeout = timeout |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
package mq |
||||||
|
|
||||||
|
// 初始化jetStream流, {streamName, topics...}
|
||||||
|
var initialStream = [][]string{ |
||||||
|
{StreamExchange, "exchange.>"}, |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
// ========================== jetstraem定义 ==========================
|
||||||
|
StreamExchange = "stream-exchange" // 游戏行为jetstraem消息流
|
||||||
|
|
||||||
|
// ========================== mq主题订阅 ==========================
|
||||||
|
// ------------- 交易所 -------------
|
||||||
|
TopicExchangeTradeInstanceInited = "exchange.trade_instance_inited" // 交易产品初始化完成
|
||||||
|
|
||||||
|
// ========================== 请求响应 ==========================
|
||||||
|
// ------------- 拉去历史k线 -------------
|
||||||
|
// RequestExchangeHistoryKline = "request.exchange.history_kline"
|
||||||
|
) |
||||||
|
|
||||||
|
// nats 空参数
|
||||||
|
var NatsNilData = map[string]any{} |
||||||
Loading…
Reference in new issue