[hw-5] concurrency, graceful shutdown, concurrent tests

This commit is contained in:
Никита Шубин
2025-07-06 20:52:27 +00:00
parent dbf8aaedcf
commit 84201fe495
23 changed files with 742 additions and 157 deletions

View File

@@ -1,7 +1,14 @@
package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/rs/zerolog/log"
"route256/loms/internal/app"
)
@@ -9,10 +16,27 @@ import (
func main() {
srv, err := app.NewApp(os.Getenv("CONFIG_FILE"))
if err != nil {
panic(err)
log.Fatal().Err(err).Msg("failed creating app")
}
if err := srv.ListenAndServe(); err != nil {
panic(err)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(ctx); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Msg("serving error")
}
}()
<-ctx.Done()
log.Info().Msg("shutdown signal received")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Error().Err(err).Msg("graceful shutdown failed")
} else {
log.Info().Msg("server stopped gracefully")
}
}

View File

@@ -8,7 +8,15 @@ VALUES
(135717466, 100, 20),
(135937324, 100, 30),
(1625903, 10000, 0),
(1148162, 100, 0);
(1148162, 100, 0),
(2958025, 100, 0),
(3596599, 100, 0),
(3618852, 100, 0),
(4288068, 100, 0),
(4465995, 100, 0),
(30816475, 100, 0),
(2618151, 100, 0);
-- +goose Down
DELETE FROM stocks
@@ -20,5 +28,12 @@ WHERE
135717466,
135937324,
1625903,
1148162
1148162,
2958025,
3596599,
3618852,
4288068,
4465995,
30816475,
2618151
);

View File

@@ -5,6 +5,7 @@ go 1.23.9
require (
github.com/gojuno/minimock/v3 v3.4.5
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
github.com/jackc/pgx v3.6.2+incompatible
github.com/jackc/pgx/v5 v5.7.5
github.com/opentracing/opentracing-go v1.2.0
github.com/ozontech/allure-go/pkg/framework v0.6.34
@@ -12,6 +13,7 @@ require (
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.37.0
go.uber.org/goleak v1.3.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
@@ -22,6 +24,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cockroachdb/apd v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
@@ -34,8 +37,11 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect

View File

@@ -8,6 +8,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
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=
@@ -42,6 +44,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gojuno/minimock/v3 v3.4.5 h1:Jcb0tEYZvVlQNtAAYpg3jCOoSwss2c1/rNugYTzj304=
@@ -55,10 +59,14 @@ 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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
@@ -71,6 +79,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
@@ -128,6 +138,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -167,6 +179,8 @@ go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKr
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

View File

@@ -30,6 +30,10 @@ import (
type App struct {
config *config.Config
controller *server.Server
grpcServer *grpc.Server
httpServer *http.Server
gwConn *grpc.ClientConn
}
func NewApp(configPath string) (*App, error) {
@@ -72,26 +76,65 @@ func NewApp(configPath string) (*App, error) {
return app, nil
}
func (app *App) ListenAndServe() error {
func (app *App) Shutdown(ctx context.Context) (err error) {
if app.httpServer != nil {
err = app.httpServer.Shutdown(ctx)
if err != nil {
log.Error().Err(err).Msgf("failed http gateway server shutdown")
}
}
done := make(chan struct{})
if app.grpcServer != nil {
go func() {
app.grpcServer.GracefulStop()
close(done)
}()
}
select {
case <-done:
case <-ctx.Done():
if app.grpcServer != nil {
app.grpcServer.Stop()
}
}
if app.gwConn != nil {
err2 := app.gwConn.Close()
if err2 != nil {
err = err2
log.Error().Err(err).Msgf("failed gateway connection close")
}
}
return err
}
func (app *App) ListenAndServe(ctx context.Context) error {
grpcAddr := fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.GRPCPort)
l, err := net.Listen("tcp", grpcAddr)
if err != nil {
return err
}
grpcServer := grpc.NewServer(
app.grpcServer = grpc.NewServer(
grpc.ChainUnaryInterceptor(
mw.Logging,
mw.Validate,
),
)
reflection.Register(grpcServer)
reflection.Register(app.grpcServer)
pb.RegisterLOMSServer(grpcServer, app.controller)
pb.RegisterLOMSServer(app.grpcServer, app.controller)
go func() {
if err = grpcServer.Serve(l); err != nil {
log.Fatal().Msgf("failed to serve: %v", err)
if err = app.grpcServer.Serve(l); err != nil {
log.Fatal().Err(err).Msg("failed to serve")
}
}()
@@ -106,21 +149,22 @@ func (app *App) ListenAndServe() error {
if err != nil {
return fmt.Errorf("grpc.NewClient: %w", err)
}
app.gwConn = conn
gwmux := runtime.NewServeMux()
if err = pb.RegisterLOMSHandler(context.Background(), gwmux, conn); err != nil {
if err = pb.RegisterLOMSHandler(ctx, gwmux, conn); err != nil {
return fmt.Errorf("pb.RegisterLOMSHandler: %w", err)
}
gwServer := &http.Server{
app.httpServer = &http.Server{
Addr: fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.HTTPPort),
Handler: gwmux,
ReadTimeout: 10 * time.Second,
}
log.Info().Msgf("Serving http loms at http://%s", gwServer.Addr)
log.Info().Msgf("Serving http loms at http://%s", app.httpServer.Addr)
return gwServer.ListenAndServe()
return app.httpServer.ListenAndServe()
}
func getPostgresPools(c *config.Config) (masterPool, replicaPool *pgxpool.Pool, err error) {

View File

@@ -1,37 +1,16 @@
[
{
"sku": 139275865,
"total_count": 65534,
"reserved": 0
},
{
"sku": 2956315,
"total_count": 100,
"reserved": 30
},
{
"sku": 1076963,
"total_count": 100,
"reserved": 35
},
{
"sku": 135717466,
"total_count": 100,
"reserved": 20
},
{
"sku": 135937324,
"total_count": 100,
"reserved": 30
},
{
"sku": 1625903,
"total_count": 10000,
"reserved": 0
},
{
"sku": 1148162,
"total_count": 100,
"reserved": 0
}
{"sku": 139275865,"total_count": 65534,"reserved": 0},
{"sku": 2956315,"total_count": 100,"reserved": 30},
{"sku": 1076963,"total_count": 100,"reserved": 35},
{"sku": 135717466,"total_count": 100,"reserved": 20},
{"sku": 135937324,"total_count": 100,"reserved": 30},
{"sku": 1625903,"total_count": 10000,"reserved": 0},
{"sku": 1148162,"total_count": 100,"reserved": 0},
{"sku": 2958025,"total_count": 100,"reserved": 0},
{"sku": 3596599,"total_count": 100,"reserved": 0},
{"sku": 3618852,"total_count": 100,"reserved": 0},
{"sku": 4288068,"total_count": 100,"reserved": 0},
{"sku": 4465995,"total_count": 100,"reserved": 0},
{"sku": 30816475,"total_count": 100,"reserved": 0},
{"sku": 2618151,"total_count": 100,"reserved": 0}
]

View File

@@ -27,7 +27,7 @@ func NewStockRepository(write, read *pgxpool.Pool) service.StockRepository {
}
}
func (s *stockRepo) GetQuerier(ctx context.Context) *Queries {
func (s *stockRepo) GetWriter(ctx context.Context) *Queries {
tx, ok := postgres.TxFromCtx(ctx)
if ok {
return New(tx)
@@ -36,8 +36,17 @@ func (s *stockRepo) GetQuerier(ctx context.Context) *Queries {
return New(s.write)
}
func (s *stockRepo) GetReader(ctx context.Context) *Queries {
tx, ok := postgres.TxFromCtx(ctx)
if ok {
return New(tx)
}
return New(s.read)
}
func (s *stockRepo) StockReserve(ctx context.Context, stock *entity.Stock) error {
querier := s.GetQuerier(ctx)
querier := s.GetWriter(ctx)
rows, err := querier.StockReserve(ctx, &StockReserveParams{
Sku: int64(stock.Item.ID),
@@ -55,7 +64,7 @@ func (s *stockRepo) StockReserve(ctx context.Context, stock *entity.Stock) error
}
func (s *stockRepo) StockReserveRemove(ctx context.Context, stock *entity.Stock) error {
querier := s.GetQuerier(ctx)
querier := s.GetWriter(ctx)
rows, err := querier.StockReserveRemove(ctx, &StockReserveRemoveParams{
Sku: int64(stock.Item.ID),
@@ -81,7 +90,7 @@ func (s *stockRepo) StockReserveRemove(ctx context.Context, stock *entity.Stock)
}
func (s *stockRepo) StockCancel(ctx context.Context, stock *entity.Stock) error {
querier := s.GetQuerier(ctx)
querier := s.GetWriter(ctx)
rows, err := querier.StockCancel(ctx, &StockCancelParams{
Sku: int64(stock.Item.ID),
@@ -107,7 +116,7 @@ func (s *stockRepo) StockCancel(ctx context.Context, stock *entity.Stock) error
}
func (s *stockRepo) StockGetByID(ctx context.Context, sku entity.Sku) (*entity.Stock, error) {
querier := s.GetQuerier(ctx)
querier := s.GetReader(ctx)
stock, err := querier.StockGetByID(ctx, int64(sku))
switch {

View File

@@ -8,6 +8,7 @@ import (
"github.com/gojuno/minimock/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"route256/loms/internal/domain/entity"
"route256/loms/internal/domain/model"
@@ -39,6 +40,10 @@ func (t *mockTxManager) ReadWithRepeatableRead(ctx context.Context, fn func(ctx
return fn(ctx)
}
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestLomsService_OrderCreate(t *testing.T) {
t.Parallel()

View File

@@ -12,11 +12,13 @@ import (
"time"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/ozontech/allure-go/pkg/framework/provider"
"github.com/ozontech/allure-go/pkg/framework/suite"
"github.com/pressly/goose/v3"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"go.uber.org/goleak"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -47,10 +49,9 @@ func startPostgres(ctx context.Context, migrationsDir string) (*pgxpool.Pool, fu
"POSTGRESQL_USERNAME": "user",
"POSTGRESQL_PASSWORD": "postgres",
"POSTGRESQL_DATABASE": "loms_test",
"POSTGRESQL_PORT": "5437",
},
ExposedPorts: []string{"5437/tcp"},
WaitingFor: wait.ForListeningPort("5437/tcp").WithStartupTimeout(30 * time.Second),
ExposedPorts: []string{"5432/tcp"},
WaitingFor: wait.ForListeningPort("5432/tcp").WithStartupTimeout(30 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx,
testcontainers.GenericContainerRequest{ContainerRequest: req, Started: true})
@@ -58,7 +59,7 @@ func startPostgres(ctx context.Context, migrationsDir string) (*pgxpool.Pool, fu
return nil, nil, err
}
endpoint, err := container.Endpoint(ctx, "")
endpoint, err := container.PortEndpoint(ctx, "5432", "")
if err != nil {
container.Terminate(ctx)
return nil, nil, err
@@ -84,6 +85,7 @@ func startPostgres(ctx context.Context, migrationsDir string) (*pgxpool.Pool, fu
container.Terminate(ctx)
return nil, nil, err
}
if err := goose.Up(std, migrationsDir); err != nil {
container.Terminate(ctx)
return nil, nil, err
@@ -109,6 +111,8 @@ type LomsIntegrationSuite struct {
}
func TestLomsIntegrationSuite(t *testing.T) {
defer goleak.VerifyNone(t)
suite.RunSuite(t, new(LomsIntegrationSuite))
}
@@ -116,6 +120,8 @@ func (s *LomsIntegrationSuite) BeforeAll(t provider.T) {
ctx := context.Background()
t.WithNewStep("init cart-service", func(sCtx provider.StepCtx) {
pool, cleanup, err := startPostgres(ctx, migrationsDir)
sCtx.Require().NoError(err, "failed postgres setup")
s.cleanup = cleanup
orderRepo := ordersRepository.NewOrderRepository(pool)