github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/components/requestreply/command_bus.go (about) 1 package requestreply 2 3 import ( 4 "context" 5 6 "github.com/pkg/errors" 7 8 "github.com/wfusion/gofusion/common/infra/watermill/message" 9 "github.com/wfusion/gofusion/common/utils" 10 ) 11 12 type CommandBus interface { 13 SendWithModifiedMessage(ctx context.Context, cmd any, modify func(*message.Message) error) error 14 } 15 16 // SendWithReply sends command to the command bus and receives a replies of the command handler. 17 // It returns a channel with replies, cancel function and error. 18 // If more than one replies are sent, only the first which is received is returned. 19 // 20 // If you expect multiple replies, please use SendWithReplies instead. 21 // 22 // SendWithReply is blocking until the first reply is received or the context is canceled. 23 // SendWithReply can be cancelled by cancelling context or 24 // by exceeding the timeout set in the backend (if set). 25 // 26 // SendWithReply can listen for handlers with results (NewCommandHandlerWithResult) and without results (NewCommandHandler). 27 // If you are listening for handlers without results, you should pass `NoResult` or `struct{}` as `Result` generic type: 28 // 29 // reply, err := requestreply.SendWithReply[requestreply.NoResult]( 30 // context.Background(), 31 // ts.CommandBus, 32 // ts.RequestReplyBackend, 33 // &TestCommand{ID: "1"}, 34 // ) 35 // 36 // If `NewCommandHandlerWithResult` handler returns a specific type, you should pass it as `Result` generic type: 37 // 38 // reply, err := requestreply.SendWithReply[SomeTypeReturnedByHandler]( 39 // context.Background(), 40 // ts.CommandBus, 41 // ts.RequestReplyBackend, 42 // &TestCommand{ID: "1"}, 43 // ) 44 func SendWithReply[Result any]( 45 ctx context.Context, 46 c CommandBus, 47 backend Backend[Result], 48 cmd any, 49 ) (Reply[Result], error) { 50 replyCh, cancel, err := SendWithReplies[Result](ctx, c, backend, cmd) 51 if err != nil { 52 return Reply[Result]{}, errors.Wrap(err, "SendWithReplies failed") 53 } 54 defer cancel() 55 56 select { 57 case <-ctx.Done(): 58 return Reply[Result]{}, errors.Wrap(ctx.Err(), "context closed") 59 case reply := <-replyCh: 60 return reply, nil 61 } 62 } 63 64 // SendWithReplies sends command to the command bus and receives a replies of the command handler. 65 // It returns a channel with replies, cancel function and error. 66 // 67 // SendWithReplies can be cancelled by calling cancel function or by cancelling context or 68 // When SendWithReplies is canceled, the returned channel is closed as well. 69 // by exceeding the timeout set in the backend (if set). 70 // Warning: It's important to cancel the function, because it's listening for the replies in the background. 71 // Lack of cancelling the function can lead to subscriber leak. 72 // 73 // SendWithReplies can listen for handlers with results (NewCommandHandlerWithResult) and without results (NewCommandHandler). 74 // If you are listening for handlers without results, you should pass `NoResult` or `struct{}` as `Result` generic type: 75 // 76 // replyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult]( 77 // context.Background(), 78 // ts.CommandBus, 79 // ts.RequestReplyBackend, 80 // &TestCommand{ID: "1"}, 81 // ) 82 // 83 // If `NewCommandHandlerWithResult` handler returns a specific type, you should pass it as `Result` generic type: 84 // 85 // replyCh, cancel, err := requestreply.SendWithReplies[SomeTypeReturnedByHandler]( 86 // context.Background(), 87 // ts.CommandBus, 88 // ts.RequestReplyBackend, 89 // &TestCommand{ID: "1"}, 90 // ) 91 // 92 // SendWithReplies will send the replies to the channel until the context is cancelled or the timeout is exceeded. 93 // They are multiple cases when more than one reply can be sent: 94 // - when the handler returns an error, and backend is configured to nack the message on error 95 // (for the PubSubBackend, it depends on `PubSubBackendConfig.AckCommandErrors` option.), 96 // - when you are using fan-out mechanism and commands are handled multiple times, 97 func SendWithReplies[Result any]( 98 ctx context.Context, 99 c CommandBus, 100 backend Backend[Result], 101 cmd any, 102 ) (replCh <-chan Reply[Result], cancel func(), err error) { 103 ctx, cancel = context.WithCancel(ctx) 104 105 defer func() { 106 if err != nil { 107 cancel() 108 } 109 }() 110 111 operationID := utils.UUID() 112 113 replyChan, err := backend.ListenForNotifications(ctx, BackendListenForNotificationsParams{ 114 Command: cmd, 115 OperationID: OperationID(operationID), 116 }) 117 if err != nil { 118 return nil, cancel, errors.Wrap(err, "cannot listen for reply") 119 } 120 121 if err := c.SendWithModifiedMessage(ctx, cmd, func(m *message.Message) error { 122 m.Metadata.Set(OperationIDMetadataKey, operationID) 123 return nil 124 }); err != nil { 125 return nil, cancel, errors.Wrap(err, "cannot send command") 126 } 127 128 return replyChan, cancel, nil 129 }