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

30
api/gateway/common/go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/tech/sendico/gateway/common
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
)
require (
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

158
api/gateway/common/go.sum Normal file
View File

@@ -0,0 +1,158 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,33 @@
package outbox
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
)
const Collection = "outbox"
type Status string
const (
StatusPending Status = "pending"
StatusSent Status = "sent"
StatusFailed Status = "failed"
)
// Event represents an outbox message pending dispatch to the broker.
type Event struct {
storable.Base `bson:",inline" json:",inline"`
EventID string `bson:"eventId" json:"eventId"`
Subject string `bson:"subject" json:"subject"`
Payload []byte `bson:"payload" json:"payload"`
Status Status `bson:"status" json:"status"`
Attempts int `bson:"attempts" json:"attempts"`
SentAt *time.Time `bson:"sentAt,omitempty" json:"sentAt,omitempty"`
}
func (*Event) Collection() string {
return Collection
}

View File

@@ -0,0 +1,123 @@
package outbox
import (
"context"
"time"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
type mongoStore struct {
logger mlogger.Logger
repo repository.Repository
}
func NewMongoStore(logger mlogger.Logger, db *mongo.Database) (Store, error) {
if db == nil {
return nil, merrors.InvalidArgument("mongo database is nil")
}
if logger == nil {
logger = zap.NewNop()
}
repo := repository.CreateMongoRepository(db, Collection)
statusIndex := &ri.Definition{
Keys: []ri.Key{{Field: "status", Sort: ri.Asc}, {Field: "createdAt", Sort: ri.Asc}},
}
if err := repo.CreateIndex(statusIndex); err != nil {
logger.Error("Failed to ensure outbox status index", zap.Error(err))
return nil, err
}
eventIDIndex := &ri.Definition{
Keys: []ri.Key{{Field: "eventId", Sort: ri.Asc}},
Unique: true,
}
if err := repo.CreateIndex(eventIDIndex); err != nil {
logger.Error("Failed to ensure outbox eventId index", zap.Error(err))
return nil, err
}
childLogger := logger.Named(Collection)
childLogger.Debug("Outbox store initialised", zap.String("collection", Collection))
return &mongoStore{logger: childLogger, repo: repo}, nil
}
func (o *mongoStore) Create(ctx context.Context, event *Event) error {
if event == nil {
o.logger.Warn("Attempt to create nil outbox event")
return merrors.InvalidArgument("outbox: nil event")
}
if err := o.repo.Insert(ctx, event, nil); err != nil {
if mongo.IsDuplicateKeyError(err) {
o.logger.Warn("Duplicate outbox event id", zap.String("event_id", event.EventID))
return merrors.DataConflict("outbox event with this id already exists")
}
o.logger.Warn("Failed to create outbox event", zap.Error(err))
return err
}
o.logger.Debug("Outbox event created", zap.String("event_id", event.EventID), zap.String("subject", event.Subject))
return nil
}
func (o *mongoStore) ListPending(ctx context.Context, limit int) ([]*Event, error) {
limit64 := int64(limit)
query := repository.Query().
Filter(repository.Field("status"), StatusPending).
Limit(&limit64).
Sort(repository.Field("createdAt"), true)
events := make([]*Event, 0)
err := o.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
doc := &Event{}
if err := cur.Decode(doc); err != nil {
return err
}
events = append(events, doc)
return nil
})
if err != nil {
o.logger.Warn("Failed to list pending outbox events", zap.Error(err))
return nil, err
}
return events, nil
}
func (o *mongoStore) MarkSent(ctx context.Context, eventRef bson.ObjectID, sentAt time.Time) error {
if eventRef.IsZero() {
return merrors.InvalidArgument("outbox: zero event id")
}
patch := repository.Patch().
Set(repository.Field("status"), StatusSent).
Set(repository.Field("sentAt"), sentAt)
return o.repo.Patch(ctx, eventRef, patch)
}
func (o *mongoStore) MarkFailed(ctx context.Context, eventRef bson.ObjectID) error {
if eventRef.IsZero() {
return merrors.InvalidArgument("outbox: zero event id")
}
patch := repository.Patch().Set(repository.Field("status"), StatusFailed)
return o.repo.Patch(ctx, eventRef, patch)
}
func (o *mongoStore) IncrementAttempts(ctx context.Context, eventRef bson.ObjectID) error {
if eventRef.IsZero() {
return merrors.InvalidArgument("outbox: zero event id")
}
patch := repository.Patch().Inc(repository.Field("attempts"), 1)
return o.repo.Patch(ctx, eventRef, patch)
}

View File

@@ -0,0 +1,108 @@
package outbox
import (
"context"
"strings"
"time"
pmessaging "github.com/tech/sendico/pkg/messaging"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger"
cfgmodel "github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
type reliableStoreAdapter struct {
store Store
}
func NewReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store Store, messagingSettings cfgmodel.SettingsT, opts ...pmessagingreliable.Option) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) {
if store == nil {
return nil, pmessagingreliable.DefaultSettings(), nil
}
producer, settings, err := pmessagingreliable.NewReliableProducerFromConfig(logger, direct, &reliableStoreAdapter{store: store}, messagingSettings, opts...)
if err != nil {
return nil, pmessagingreliable.Settings{}, err
}
return producer, settings, nil
}
func (a *reliableStoreAdapter) Enqueue(ctx context.Context, msg pmessagingreliable.OutboxMessage) error {
if a == nil || a.store == nil {
return nil
}
return a.store.Create(ctx, &Event{
EventID: strings.TrimSpace(msg.EventID),
Subject: strings.TrimSpace(msg.Subject),
Payload: append([]byte(nil), msg.Payload...),
Status: StatusPending,
Attempts: msg.Attempts,
})
}
func (a *reliableStoreAdapter) ListPending(ctx context.Context, limit int) ([]pmessagingreliable.OutboxMessage, error) {
if a == nil || a.store == nil {
return nil, nil
}
events, err := a.store.ListPending(ctx, limit)
if err != nil {
return nil, err
}
result := make([]pmessagingreliable.OutboxMessage, 0, len(events))
for _, event := range events {
if event == nil {
continue
}
reference := ""
if eventRef := event.GetID(); eventRef != nil && !eventRef.IsZero() {
reference = eventRef.Hex()
}
result = append(result, pmessagingreliable.OutboxMessage{
Reference: reference,
EventID: strings.TrimSpace(event.EventID),
Subject: strings.TrimSpace(event.Subject),
Payload: append([]byte(nil), event.Payload...),
Attempts: event.Attempts,
CreatedAt: event.CreatedAt,
})
}
return result, nil
}
func (a *reliableStoreAdapter) MarkSent(ctx context.Context, reference string, sentAt time.Time) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.MarkSent(ctx, eventRef, sentAt)
}
func (a *reliableStoreAdapter) MarkFailed(ctx context.Context, reference string) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.MarkFailed(ctx, eventRef)
}
func (a *reliableStoreAdapter) IncrementAttempts(ctx context.Context, reference string) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.IncrementAttempts(ctx, eventRef)
}
func parseObjectID(raw string) (bson.ObjectID, error) {
return bson.ObjectIDFromHex(raw)
}

View File

@@ -0,0 +1,330 @@
package outbox
import (
"context"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
domainmodel "github.com/tech/sendico/pkg/model"
notification "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func TestGatewayReliableProducerPersistsAndRetriesOnBrokerFailure(t *testing.T) {
store := newMemoryOutboxStore()
broker := &flakyDirectProducer{failuresRemaining: 1}
producer, _, err := NewReliableProducer(
zap.NewNop(),
broker,
store,
nil,
pmessagingreliable.WithBatchSize(1),
pmessagingreliable.WithMaxAttempts(3),
)
if err != nil {
t.Fatalf("failed to create reliable producer: %v", err)
}
env := newTestEnvelope(t, []byte(`{"transferRef":"tx-1","status":"pending"}`))
if err := producer.SendWithOutbox(context.Background(), env); err != nil {
t.Fatalf("failed to enqueue envelope into outbox: %v", err)
}
eventID := env.GetMessageId().String()
persisted := store.EventByID(eventID)
if persisted == nil {
t.Fatalf("expected outbox event %s to be persisted", eventID)
}
if persisted.Status != StatusPending {
t.Fatalf("expected pending status after enqueue, got %q", persisted.Status)
}
if persisted.Attempts != 0 {
t.Fatalf("expected zero attempts after enqueue, got %d", persisted.Attempts)
}
processed, err := producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("first dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected first dispatch to process 1 event, got %d", processed)
}
afterFailure := store.EventByID(eventID)
if afterFailure == nil {
t.Fatalf("expected outbox event %s to exist after broker failure", eventID)
}
if afterFailure.Status != StatusPending {
t.Fatalf("expected event to stay pending after transient broker error, got %q", afterFailure.Status)
}
if afterFailure.Attempts != 1 {
t.Fatalf("expected attempts to increment to 1 after failure, got %d", afterFailure.Attempts)
}
if afterFailure.SentAt != nil {
t.Fatalf("expected sentAt to be empty after failed publish")
}
processed, err = producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("second dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected second dispatch to process 1 event, got %d", processed)
}
afterRetry := store.EventByID(eventID)
if afterRetry == nil {
t.Fatalf("expected outbox event %s to exist after retry", eventID)
}
if afterRetry.Status != StatusSent {
t.Fatalf("expected event to be sent after retry, got %q", afterRetry.Status)
}
if afterRetry.Attempts != 1 {
t.Fatalf("expected attempts to remain 1 after successful retry, got %d", afterRetry.Attempts)
}
if afterRetry.SentAt == nil {
t.Fatalf("expected sentAt to be set after successful publish")
}
if attempts := broker.Attempts(); attempts != 2 {
t.Fatalf("expected two broker attempts (fail then success), got %d", attempts)
}
}
func TestGatewayReliableProducerMarksFailedAfterMaxAttempts(t *testing.T) {
store := newMemoryOutboxStore()
broker := &flakyDirectProducer{failuresRemaining: 10}
producer, _, err := NewReliableProducer(
zap.NewNop(),
broker,
store,
nil,
pmessagingreliable.WithBatchSize(1),
pmessagingreliable.WithMaxAttempts(2),
)
if err != nil {
t.Fatalf("failed to create reliable producer: %v", err)
}
env := newTestEnvelope(t, []byte(`{"transferRef":"tx-2","status":"pending"}`))
if err := producer.SendWithOutbox(context.Background(), env); err != nil {
t.Fatalf("failed to enqueue envelope into outbox: %v", err)
}
eventID := env.GetMessageId().String()
processed, err := producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("first dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected first dispatch to process 1 event, got %d", processed)
}
processed, err = producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("second dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected second dispatch to process 1 event, got %d", processed)
}
processed, err = producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("third dispatch failed: %v", err)
}
if processed != 0 {
t.Fatalf("expected failed event to be excluded from pending queue, got processed=%d", processed)
}
final := store.EventByID(eventID)
if final == nil {
t.Fatalf("expected outbox event %s to exist", eventID)
}
if final.Status != StatusFailed {
t.Fatalf("expected event to be marked failed after max attempts, got %q", final.Status)
}
if final.Attempts != 2 {
t.Fatalf("expected attempts to equal max attempts (2), got %d", final.Attempts)
}
if final.SentAt != nil {
t.Fatalf("expected sentAt to remain empty for failed event")
}
}
func newTestEnvelope(t *testing.T, payload []byte) me.Envelope {
t.Helper()
env := me.CreateEnvelope("gateway.common.outbox.test", domainmodel.NewNotification(mservice.ChainGateway, notification.NAUpdated))
if _, err := env.Wrap(payload); err != nil {
t.Fatalf("failed to wrap test payload: %v", err)
}
return env
}
type memoryOutboxStore struct {
mu sync.Mutex
eventsByRef map[bson.ObjectID]*Event
refByEvent map[string]bson.ObjectID
}
func newMemoryOutboxStore() *memoryOutboxStore {
return &memoryOutboxStore{
eventsByRef: make(map[bson.ObjectID]*Event),
refByEvent: make(map[string]bson.ObjectID),
}
}
func (s *memoryOutboxStore) Create(_ context.Context, event *Event) error {
if event == nil {
return errors.New("event is nil")
}
s.mu.Lock()
defer s.mu.Unlock()
eventID := strings.TrimSpace(event.EventID)
if eventID == "" {
return errors.New("event id is required")
}
if _, exists := s.refByEvent[eventID]; exists {
return errors.New("duplicate event id")
}
stored := cloneEvent(event)
stored.SetID(bson.NewObjectID())
if stored.Status == "" {
stored.Status = StatusPending
}
ref := *stored.GetID()
s.eventsByRef[ref] = stored
s.refByEvent[eventID] = ref
return nil
}
func (s *memoryOutboxStore) ListPending(_ context.Context, limit int) ([]*Event, error) {
s.mu.Lock()
defer s.mu.Unlock()
pending := make([]*Event, 0, len(s.eventsByRef))
for _, event := range s.eventsByRef {
if event.Status == StatusPending {
pending = append(pending, cloneEvent(event))
}
}
sort.Slice(pending, func(i, j int) bool {
return pending[i].CreatedAt.Before(pending[j].CreatedAt)
})
if limit > 0 && len(pending) > limit {
pending = pending[:limit]
}
return pending, nil
}
func (s *memoryOutboxStore) MarkSent(_ context.Context, eventRef bson.ObjectID, sentAt time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
event, ok := s.eventsByRef[eventRef]
if !ok {
return errors.New("event not found")
}
event.Status = StatusSent
when := sentAt.UTC()
event.SentAt = &when
event.Update()
return nil
}
func (s *memoryOutboxStore) MarkFailed(_ context.Context, eventRef bson.ObjectID) error {
s.mu.Lock()
defer s.mu.Unlock()
event, ok := s.eventsByRef[eventRef]
if !ok {
return errors.New("event not found")
}
event.Status = StatusFailed
event.Update()
return nil
}
func (s *memoryOutboxStore) IncrementAttempts(_ context.Context, eventRef bson.ObjectID) error {
s.mu.Lock()
defer s.mu.Unlock()
event, ok := s.eventsByRef[eventRef]
if !ok {
return errors.New("event not found")
}
event.Attempts++
event.Update()
return nil
}
func (s *memoryOutboxStore) EventByID(eventID string) *Event {
s.mu.Lock()
defer s.mu.Unlock()
ref, ok := s.refByEvent[eventID]
if !ok {
return nil
}
event, ok := s.eventsByRef[ref]
if !ok {
return nil
}
return cloneEvent(event)
}
func cloneEvent(event *Event) *Event {
if event == nil {
return nil
}
copyEvent := *event
copyEvent.Payload = append([]byte(nil), event.Payload...)
if event.SentAt != nil {
sentAt := *event.SentAt
copyEvent.SentAt = &sentAt
}
return &copyEvent
}
type flakyDirectProducer struct {
mu sync.Mutex
failuresRemaining int
attempts int
}
func (p *flakyDirectProducer) SendMessage(_ me.Envelope) error {
p.mu.Lock()
defer p.mu.Unlock()
p.attempts++
if p.failuresRemaining > 0 {
p.failuresRemaining--
return errors.New("broker unavailable")
}
return nil
}
func (p *flakyDirectProducer) Attempts() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.attempts
}

View File

@@ -0,0 +1,72 @@
package outbox
import (
"context"
"sync"
"github.com/tech/sendico/pkg/merrors"
pmessaging "github.com/tech/sendico/pkg/messaging"
me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger"
cfgmodel "github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
// ReliableRuntime owns a reliable producer lifecycle for gateway outbox dispatch.
type ReliableRuntime struct {
once sync.Once
cancel context.CancelFunc
producer *pmessagingreliable.ReliableProducer
settings pmessagingreliable.Settings
initErr error
}
func (r *ReliableRuntime) Start(logger mlogger.Logger, direct pmessaging.Producer, store Store, messagingSettings cfgmodel.SettingsT, opts ...pmessagingreliable.Option) error {
if r == nil {
return nil
}
if logger == nil {
logger = zap.NewNop()
}
logger = logger.Named("outbox_reliable")
r.once.Do(func() {
reliableProducer, settings, err := NewReliableProducer(logger, direct, store, messagingSettings, opts...)
if err != nil {
r.initErr = err
return
}
r.producer = reliableProducer
r.settings = settings
if r.producer == nil || direct == nil {
logger.Info("Outbox reliable publisher disabled", zap.Bool("enabled", settings.Enabled))
return
}
logger.Info("Outbox reliable publisher configured",
zap.Bool("enabled", settings.Enabled),
zap.Int("batch_size", settings.BatchSize),
zap.Int("poll_interval_seconds", settings.PollIntervalSeconds),
zap.Int("max_attempts", settings.MaxAttempts))
ctx, cancel := context.WithCancel(context.Background())
r.cancel = cancel
go r.producer.Run(ctx)
})
return r.initErr
}
func (r *ReliableRuntime) Send(ctx context.Context, envelope me.Envelope) error {
if r == nil || r.producer == nil {
return merrors.Internal("reliable outbox producer is not configured")
}
return r.producer.SendWithOutbox(ctx, envelope)
}
func (r *ReliableRuntime) Stop() {
if r == nil || r.cancel == nil {
return
}
r.cancel()
}

View File

@@ -0,0 +1,17 @@
package outbox
import (
"context"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
)
// Store persists gateway outbox events.
type Store interface {
Create(ctx context.Context, event *Event) error
ListPending(ctx context.Context, limit int) ([]*Event, error)
MarkSent(ctx context.Context, eventRef bson.ObjectID, sentAt time.Time) error
MarkFailed(ctx context.Context, eventRef bson.ObjectID) error
IncrementAttempts(ctx context.Context, eventRef bson.ObjectID) error
}