outbox for gateways
This commit is contained in:
@@ -162,6 +162,9 @@ func (i *Imp) Start() error {
|
||||
gatewayservice.WithDriverRegistry(driverRegistry),
|
||||
gatewayservice.WithSettings(cfg.Settings),
|
||||
}
|
||||
if cfg.Messaging != nil {
|
||||
opts = append(opts, gatewayservice.WithMessagingSettings(cfg.Messaging.Settings))
|
||||
}
|
||||
svc := gatewayservice.NewService(logger, repo, producer, opts...)
|
||||
i.service = svc
|
||||
return svc, nil
|
||||
|
||||
@@ -91,3 +91,12 @@ func WithDiscoveryInvokeURI(invokeURI string) Option {
|
||||
s.invokeURI = strings.TrimSpace(invokeURI)
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessagingSettings applies messaging driver settings.
|
||||
func WithMessagingSettings(settings pmodel.SettingsT) Option {
|
||||
return func(s *Service) {
|
||||
if settings != nil {
|
||||
s.msgCfg = settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||
)
|
||||
|
||||
type chainOutboxProvider interface {
|
||||
Outbox() gatewayoutbox.Store
|
||||
}
|
||||
|
||||
type chainTransactionProvider interface {
|
||||
TransactionFactory() transaction.Factory
|
||||
}
|
||||
|
||||
func (s *Service) outboxStore() gatewayoutbox.Store {
|
||||
provider, ok := s.storage.(chainOutboxProvider)
|
||||
if !ok || provider == nil {
|
||||
return nil
|
||||
}
|
||||
return provider.Outbox()
|
||||
}
|
||||
|
||||
func (s *Service) startOutboxReliableProducer() error {
|
||||
if s == nil || s.storage == nil {
|
||||
return nil
|
||||
}
|
||||
return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg)
|
||||
}
|
||||
|
||||
func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error {
|
||||
if err := s.startOutboxReliableProducer(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.outbox.Send(ctx, env)
|
||||
}
|
||||
|
||||
func (s *Service) executeTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
provider, ok := s.storage.(chainTransactionProvider)
|
||||
if !ok || provider == nil || provider.TransactionFactory() == nil {
|
||||
return cb(ctx)
|
||||
}
|
||||
return provider.TransactionFactory().CreateTransaction().Execute(ctx, cb)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||
"github.com/tech/sendico/gateway/chain/storage"
|
||||
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||
"github.com/tech/sendico/pkg/api/routers"
|
||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -40,9 +42,11 @@ type Service struct {
|
||||
logger mlogger.Logger
|
||||
storage storage.Repository
|
||||
producer msg.Producer
|
||||
msgCfg pmodel.SettingsT
|
||||
clock clockpkg.Clock
|
||||
|
||||
settings CacheSettings
|
||||
outbox gatewayoutbox.ReliableRuntime
|
||||
|
||||
networks map[pmodel.ChainNetwork]shared.Network
|
||||
serviceWallet shared.ServiceWallet
|
||||
@@ -63,6 +67,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
logger: logger.Named("service"),
|
||||
storage: repo,
|
||||
producer: producer,
|
||||
msgCfg: map[string]any{},
|
||||
clock: clockpkg.System{},
|
||||
settings: defaultSettings(),
|
||||
networks: map[pmodel.ChainNetwork]shared.Network{},
|
||||
@@ -84,6 +89,9 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
}
|
||||
svc.settings = svc.settings.withDefaults()
|
||||
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
|
||||
if err := svc.startOutboxReliableProducer(); err != nil {
|
||||
svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err))
|
||||
}
|
||||
|
||||
svc.commands = commands.NewRegistry(commands.RegistryDeps{
|
||||
Wallet: commandsWalletDeps(svc),
|
||||
@@ -105,6 +113,7 @@ func (s *Service) Shutdown() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.outbox.Stop()
|
||||
for _, announcer := range s.announcers {
|
||||
if announcer != nil {
|
||||
announcer.Stop()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
@@ -13,6 +14,9 @@ import (
|
||||
)
|
||||
|
||||
func isFinalStatus(t *model.Transfer) bool {
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
switch t.Status {
|
||||
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
|
||||
return true
|
||||
@@ -21,16 +25,25 @@ func isFinalStatus(t *model.Transfer) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func toOpStatus(t *model.Transfer) rail.OperationResult {
|
||||
func isFinalTransferStatus(status model.TransferStatus) bool {
|
||||
switch status {
|
||||
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func toOpStatus(t *model.Transfer) (rail.OperationResult, error) {
|
||||
switch t.Status {
|
||||
case model.TransferStatusFailed:
|
||||
return rail.OperationResultFailed
|
||||
return rail.OperationResultFailed, nil
|
||||
case model.TransferStatusSuccess:
|
||||
return rail.OperationResultSuccess
|
||||
return rail.OperationResultSuccess, nil
|
||||
case model.TransferStatusCancelled:
|
||||
return rail.OperationResultCancelled
|
||||
return rail.OperationResultCancelled, nil
|
||||
default:
|
||||
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status))
|
||||
return rail.OperationResultFailed, merrors.InvalidArgument(fmt.Sprintf("unexpected transfer status: %s", t.Status), "transfer.status")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,19 +58,47 @@ func toError(t *model.Transfer) string {
|
||||
}
|
||||
|
||||
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
|
||||
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
||||
if !isFinalTransferStatus(status) {
|
||||
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
|
||||
}
|
||||
return transfer, err
|
||||
}
|
||||
|
||||
res, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) {
|
||||
transfer, statusErr := s.storage.Transfers().UpdateStatus(txCtx, transferRef, status, failureReason, txHash)
|
||||
if statusErr != nil {
|
||||
return nil, statusErr
|
||||
}
|
||||
if isFinalStatus(transfer) {
|
||||
if emitErr := s.emitTransferStatusEvent(txCtx, transfer); emitErr != nil {
|
||||
return nil, emitErr
|
||||
}
|
||||
}
|
||||
return transfer, nil
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if isFinalStatus(transfer) {
|
||||
s.emitTransferStatusEvent(transfer)
|
||||
}
|
||||
return transfer, err
|
||||
|
||||
transfer, _ := res.(*model.Transfer)
|
||||
return transfer, nil
|
||||
}
|
||||
|
||||
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
|
||||
if s == nil || s.producer == nil || transfer == nil {
|
||||
return
|
||||
func (s *Service) emitTransferStatusEvent(ctx context.Context, transfer *model.Transfer) error {
|
||||
if s == nil || transfer == nil {
|
||||
return nil
|
||||
}
|
||||
if s.producer == nil || s.outboxStore() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
status, err := toOpStatus(transfer)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to map transfer status for transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
||||
return err
|
||||
}
|
||||
|
||||
exec := pmodel.PaymentGatewayExecution{
|
||||
@@ -65,13 +106,15 @@ func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
|
||||
IdempotencyKey: transfer.IdempotencyKey,
|
||||
ExecutedMoney: transfer.NetAmount,
|
||||
PaymentRef: transfer.PaymentRef,
|
||||
Status: toOpStatus(transfer),
|
||||
Status: status,
|
||||
OperationRef: transfer.OperationRef,
|
||||
Error: toError(transfer),
|
||||
TransferRef: transfer.TransferRef,
|
||||
}
|
||||
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
|
||||
if err := s.producer.SendMessage(env); err != nil {
|
||||
if err := s.sendWithOutbox(ctx, env); err != nil {
|
||||
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user