outbox for gateways

This commit is contained in:
Stephan D
2026-02-18 01:35:28 +01:00
parent 974caf286c
commit 69531cee73
221 changed files with 12172 additions and 782 deletions

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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
}