github.com/wfusion/gofusion@v1.1.14/mq/mq.go (about)

     1  package mq
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  
     8  	"github.com/Rican7/retry"
     9  
    10  	"github.com/wfusion/gofusion/common/infra/watermill"
    11  	"github.com/wfusion/gofusion/common/utils"
    12  	"github.com/wfusion/gofusion/common/utils/clone"
    13  	"github.com/wfusion/gofusion/common/utils/compress"
    14  	"github.com/wfusion/gofusion/common/utils/serialize"
    15  	"github.com/wfusion/gofusion/routine"
    16  
    17  	mw "github.com/wfusion/gofusion/common/infra/watermill/message"
    18  	fusCtx "github.com/wfusion/gofusion/context"
    19  	pd "github.com/wfusion/gofusion/internal/util/payload"
    20  )
    21  
    22  type abstractMQ struct {
    23  	pub mw.Publisher
    24  	sub mw.Subscriber
    25  
    26  	appName string
    27  	ctx     context.Context
    28  	name    string
    29  	conf    *Conf
    30  	logger  watermill.LoggerAdapter
    31  
    32  	compressType  compress.Algorithm
    33  	serializeType serialize.Algorithm
    34  }
    35  
    36  func newPub(ctx context.Context, pub mw.Publisher, appName, name string,
    37  	conf *Conf, logger watermill.LoggerAdapter) *abstractMQ {
    38  	mq := &abstractMQ{ctx: ctx, pub: pub, appName: appName, name: name, conf: clone.Slowly(conf), logger: logger}
    39  	mq.serializeType = serialize.ParseAlgorithm(conf.SerializeType)
    40  	mq.compressType = compress.ParseAlgorithm(conf.CompressType)
    41  	return mq
    42  }
    43  
    44  func newSub(ctx context.Context, sub mw.Subscriber, appName, name string,
    45  	conf *Conf, logger watermill.LoggerAdapter) *abstractMQ {
    46  	mq := &abstractMQ{ctx: ctx, sub: sub, appName: appName, name: name, conf: clone.Slowly(conf), logger: logger}
    47  	mq.serializeType = serialize.ParseAlgorithm(conf.SerializeType)
    48  	mq.compressType = compress.ParseAlgorithm(conf.CompressType)
    49  	return mq
    50  }
    51  
    52  func (a *abstractMQ) Publish(ctx context.Context, opts ...utils.OptionExtender) (err error) {
    53  	opt := utils.ApplyOptions[pubOption](opts...)
    54  	msgs := opt.watermillMessages
    55  	for _, msg := range opt.messages {
    56  		msg, err := a.newMessage(ctx, msg, opt)
    57  		if err != nil {
    58  			return err
    59  		}
    60  		msgs = append(msgs, msg)
    61  	}
    62  	for _, object := range opt.objects {
    63  		msg, err := a.newObjectMessage(ctx, object, opt)
    64  		if err != nil {
    65  			return err
    66  		}
    67  		msgs = append(msgs, msg)
    68  	}
    69  	if len(msgs) == 0 {
    70  		logInfo(ctx, a.logger, a.appName, a.name, "none messages to publish")
    71  		return
    72  	}
    73  
    74  	if !opt.async {
    75  		return a.pub.Publish(ctx, a.conf.Topic, msgs...)
    76  	}
    77  
    78  	routine.Goc(ctx, func() {
    79  		idList := utils.SliceMapping(msgs, func(s *mw.Message) (id string) { return s.UUID })
    80  		idList = utils.NewSet(idList...).Items()
    81  		retryFunc := func(attempt uint) error {
    82  			if attempt > 1 {
    83  				logInfo(ctx, a.logger, a.appName, a.name,
    84  					"retry to publish topic message [topic[%s] message%v[%v] attempt[%v]]",
    85  					a.conf.Topic, idList, len(msgs), attempt-1)
    86  			}
    87  			return a.pub.Publish(ctx, a.conf.Topic, msgs...)
    88  		}
    89  
    90  		if err = retry.Retry(retryFunc, opt.asyncStrategies...); err != nil {
    91  			logError(ctx, a.logger, a.appName, a.name,
    92  				"retry to publish topic message failed [err[%s] topic[%s] message%v[%v]]",
    93  				err, a.conf.Topic, idList, len(msgs))
    94  		}
    95  	}, routine.AppName(a.appName))
    96  	return
    97  }
    98  
    99  func (a *abstractMQ) PublishRaw(ctx context.Context, opts ...utils.OptionExtender) (err error) {
   100  	opt := utils.ApplyOptions[pubOption](opts...)
   101  	msgs := opt.watermillMessages
   102  	for _, msg := range opt.messages {
   103  		wmsg := mw.NewMessage(msg.ID(), msg.Payload())
   104  		wmsg.Metadata = fusCtx.WatermillMetadata(ctx)
   105  		wmsg.SetContext(ctx)
   106  		msgs = append(msgs, wmsg)
   107  	}
   108  	if len(msgs) == 0 {
   109  		logInfo(ctx, a.logger, a.appName, a.name, "none messages to publish")
   110  		return
   111  	}
   112  
   113  	if !opt.async {
   114  		return a.pub.Publish(ctx, a.conf.Topic, msgs...)
   115  	}
   116  
   117  	routine.Goc(ctx, func() {
   118  		idList := utils.SliceMapping(msgs, func(s *mw.Message) (id string) { return s.UUID })
   119  		idList = utils.NewSet(idList...).Items()
   120  		retryFunc := func(attempt uint) error {
   121  			if attempt > 1 {
   122  				logInfo(ctx, a.logger, a.appName, a.name,
   123  					"retry to publish topic message [topic[%s] message%v[%v] attempt[%v]]",
   124  					a.conf.Topic, idList, len(msgs), attempt-1)
   125  			}
   126  			return a.pub.Publish(ctx, a.conf.Topic, msgs...)
   127  		}
   128  
   129  		if err = retry.Retry(retryFunc, opt.asyncStrategies...); err != nil {
   130  			logError(ctx, a.logger, a.appName, a.name,
   131  				"retry to publish topic message failed [err[%s] topic[%s] message%v[%v]]",
   132  				err, a.conf.Topic, idList, len(msgs))
   133  		}
   134  	}, routine.AppName(a.appName))
   135  	return
   136  }
   137  
   138  func (a *abstractMQ) SubscribeRaw(ctx context.Context, opts ...utils.OptionExtender) (dst <-chan Message, err error) {
   139  	opt := utils.ApplyOptions[subOption](opts...)
   140  	ch, err := a.sub.Subscribe(ctx, a.conf.Topic)
   141  	if err != nil {
   142  		return
   143  	}
   144  
   145  	msgCh := make(chan Message, opt.channelLength)
   146  	routine.Go(func() {
   147  		defer close(msgCh)
   148  		for {
   149  			select {
   150  			case wmsg, ok := <-ch:
   151  				if !ok {
   152  					return
   153  				}
   154  				msg := rawMessageConvertFrom(wmsg)
   155  				select {
   156  				case msgCh <- msg:
   157  				case <-ctx.Done():
   158  					msg.Nack()
   159  					a.logger.Info(fmt.Sprintf(
   160  						"raw subscriber %s exited with a message nacked when business ctx done", a.name),
   161  						watermill.LogFields{watermill.ContextLogFieldKey: ctx})
   162  					return
   163  				case <-a.ctx.Done():
   164  					msg.Nack()
   165  					a.logger.Info(fmt.Sprintf(
   166  						"raw subscriber %s exited with a message nacked when app ctx done", a.name),
   167  						watermill.LogFields{watermill.ContextLogFieldKey: ctx})
   168  					return
   169  				}
   170  			case <-ctx.Done():
   171  				return
   172  			case <-a.ctx.Done():
   173  				return
   174  			}
   175  		}
   176  	}, routine.AppName(a.appName))
   177  	return msgCh, err
   178  }
   179  
   180  func (a *abstractMQ) Subscribe(ctx context.Context, opts ...utils.OptionExtender) (dst <-chan Message, err error) {
   181  	opt := utils.ApplyOptions[subOption](opts...)
   182  	ch, err := a.sub.Subscribe(ctx, a.conf.Topic)
   183  	if err != nil {
   184  		return
   185  	}
   186  
   187  	msgCh := make(chan Message, opt.channelLength)
   188  	routine.Go(func() {
   189  		defer close(msgCh)
   190  		for {
   191  			select {
   192  			case wmsg, ok := <-ch:
   193  				if !ok {
   194  					return
   195  				}
   196  				_, data, isRaw, err := pd.UnsealRaw(wmsg.Payload, pd.Compress(a.compressType))
   197  				if err != nil {
   198  					a.logger.Error("unseal message failed", err, watermill.LogFields{
   199  						watermill.ContextLogFieldKey: ctx,
   200  					})
   201  					continue
   202  				}
   203  				wmsg.SetContext(fusCtx.New(fusCtx.Watermill(wmsg.Metadata)))
   204  				msg := &message{Message: wmsg, payload: data}
   205  				if !isRaw {
   206  					_, msg.obj, _, err = pd.Unseal(wmsg.Payload,
   207  						pd.Serialize(a.serializeType), pd.Compress(a.compressType))
   208  					if err != nil {
   209  						a.logger.Error("unseal message object failed", err, watermill.LogFields{
   210  							watermill.ContextLogFieldKey: ctx,
   211  						})
   212  						continue
   213  					}
   214  				}
   215  				select {
   216  				case msgCh <- msg:
   217  				case <-ctx.Done():
   218  					msg.Nack()
   219  					a.logger.Info(fmt.Sprintf(
   220  						"subscriber %s exited with a message nacked when business ctx done", a.name),
   221  						watermill.LogFields{watermill.ContextLogFieldKey: ctx})
   222  					return
   223  				case <-a.ctx.Done():
   224  					msg.Nack()
   225  					a.logger.Info(fmt.Sprintf(
   226  						"subscriber %s exited with a message nacked when app ctx done", a.name),
   227  						watermill.LogFields{watermill.ContextLogFieldKey: ctx})
   228  					return
   229  				}
   230  
   231  			case <-ctx.Done():
   232  				return
   233  			case <-a.ctx.Done():
   234  				return
   235  			}
   236  		}
   237  	}, routine.AppName(a.appName))
   238  	return msgCh, err
   239  }
   240  
   241  func (a *abstractMQ) close() error                             { panic(ErrNotImplement) }
   242  func (a *abstractMQ) topic() string                            { return a.conf.Topic }
   243  func (a *abstractMQ) watermillPublisher() mw.Publisher         { return a.pub }
   244  func (a *abstractMQ) watermillSubscriber() mw.Subscriber       { return a.sub }
   245  func (a *abstractMQ) watermillLogger() watermill.LoggerAdapter { return a.logger }
   246  
   247  func (a *abstractMQ) newMessage(ctx context.Context, src Message, _ *pubOption) (
   248  	msg *mw.Message, err error) {
   249  	payload, err := pd.Seal(src.Payload(), pd.Compress(a.compressType))
   250  	if err != nil {
   251  		return
   252  	}
   253  	msg = mw.NewMessage(src.ID(), payload)
   254  	msg.Metadata = fusCtx.WatermillMetadata(ctx)
   255  	msg.SetContext(ctx)
   256  	return
   257  }
   258  func (a *abstractMQ) newObjectMessage(ctx context.Context, object any, opt *pubOption) (
   259  	msg *mw.Message, err error) {
   260  	payload, err := pd.Seal(object, pd.Compress(a.compressType), pd.Serialize(a.serializeType))
   261  	if err != nil {
   262  		return
   263  	}
   264  	uuid := utils.ULID()
   265  	if opt.objectUUIDGenFunc.IsValid() && opt.objectUUIDGenFunc.Kind() == reflect.Func {
   266  		inType := opt.objectUUIDGenFunc.Type().In(0)
   267  		inParam := reflect.ValueOf(object).Convert(inType)
   268  		uuid = opt.objectUUIDGenFunc.Call([]reflect.Value{inParam})[0].Interface().(string)
   269  	}
   270  	msg = mw.NewMessage(uuid, payload)
   271  	msg.Metadata = fusCtx.WatermillMetadata(ctx)
   272  	msg.SetContext(ctx)
   273  	return
   274  }
   275  
   276  func rawMessageConvertFrom(src *mw.Message) (dst Message) {
   277  	return &message{Message: src, payload: src.Payload}
   278  }
   279  
   280  func messageConvertTo(src Message) (dst *mw.Message) {
   281  	dst = src.(*message).Message
   282  	return
   283  }
   284  
   285  func messageConvertFrom(src *mw.Message,
   286  	serializeType serialize.Algorithm, compressType compress.Algorithm) (dst Message, err error) {
   287  	_, data, isRaw, err := pd.UnsealRaw(src.Payload, pd.Compress(compressType))
   288  	if err != nil {
   289  		return
   290  	}
   291  	src.SetContext(fusCtx.New(fusCtx.Watermill(src.Metadata)))
   292  	msg := &message{Message: src, payload: data}
   293  	if !isRaw {
   294  		_, msg.obj, _, err = pd.Unseal(src.Payload,
   295  			pd.Serialize(serializeType), pd.Compress(compressType))
   296  		if err != nil {
   297  			return
   298  		}
   299  	}
   300  	dst = msg
   301  	return
   302  }