From c3def9dc26d92e51d20927c057432a5bfe67c7d8 Mon Sep 17 00:00:00 2001 From: ITQ Date: Thu, 13 Nov 2025 16:09:56 +0300 Subject: [PATCH] chore: project refactor --- cmd/server/main.go | 8 ++--- go.mod | 3 ++ go.sum | 51 +++++++++++++++++++++++++++ internal/config/config.go | 10 ++++-- internal/domain/id.go | 9 +++++ internal/handler/grpc/errors.go | 2 +- internal/handler/grpc/order.go | 23 ++++++++++-- internal/interceptor/logger.go | 10 +++--- internal/repository/inmemory/order.go | 12 ++++--- internal/repository/order.go | 6 ++-- internal/repository/postgres/order.go | 49 +++++++++---------------- internal/server/server.go | 46 ++++++++++++++++++------ internal/service/order.go | 6 ++-- 13 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 internal/domain/id.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 0deee27..c8438ed 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -13,7 +13,7 @@ import ( func main() { cfg, err := config.Load() if err != nil { - log.Fatalf("Failed to load config: %v", err) + log.Fatalf("failed to load config: %v", err) } srv := server.New(cfg) @@ -21,7 +21,7 @@ func main() { go func() { if err := srv.Start(); err != nil { - log.Fatalf("Failed to start server: %v", err) + log.Fatalf("failed to start server: %v", err) } }() @@ -29,7 +29,7 @@ func main() { signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Println("Shutting down server...") + log.Println("shutting down server...") srv.Stop() - log.Println("Server stopped") + log.Println("server stopped") } diff --git a/go.mod b/go.mod index a685be3..cf296f8 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 toolchain go1.24.9 require ( + github.com/golang-migrate/migrate/v4 v4.19.0 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 github.com/jmoiron/sqlx v1.4.0 @@ -18,6 +19,8 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index d2d9396..bb00365 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,46 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 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/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +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 v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= +github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -23,6 +49,11 @@ 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.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -35,12 +66,30 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +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/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/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/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= @@ -74,3 +123,5 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 67ccdfa..dab27b5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "log" + "net" "os" "strconv" @@ -33,7 +34,7 @@ func Load() (*Config, error) { HTTPPort: mustGetInt("HTTP_PORT", 8080), //nolint:mnd // false-positive LogLevel: getEnv("LOG_LEVEL", "info"), DBHost: getEnv("POSTGRES_HOST", "localhost"), - DBPort: mustGetInt("POSTGRES_PORT", 5432), + DBPort: mustGetInt("POSTGRES_PORT", 5432), //nolint:mnd // false-positive DBUser: getEnv("POSTGRES_USERNAME", "postgres"), DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"), @@ -66,7 +67,12 @@ func mustGetBool(key string, def bool) bool { return b } -func (c Config) BuildDsn() string { +func (c Config) BuildPostgresConnStr() string { return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName) } + +func (c Config) BuildPostgresDSN() string { + return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", + c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName) +} diff --git a/internal/domain/id.go b/internal/domain/id.go new file mode 100644 index 0000000..beb671d --- /dev/null +++ b/internal/domain/id.go @@ -0,0 +1,9 @@ +package domain + +import ( + "errors" +) + +var ( + ErrInvalidID = errors.New("invalid uuid") +) diff --git a/internal/handler/grpc/errors.go b/internal/handler/grpc/errors.go index 071fc13..313ecd5 100644 --- a/internal/handler/grpc/errors.go +++ b/internal/handler/grpc/errors.go @@ -17,7 +17,7 @@ func mapError(err error) error { if errors.Is(err, domain.ErrOrderAlreadyExist) { return status.Error(codes.AlreadyExists, err.Error()) } - if errors.Is(err, domain.ErrInvalidOrderData) { + if errors.Is(err, domain.ErrInvalidOrderData) || errors.Is(err, domain.ErrInvalidID) { return status.Error(codes.InvalidArgument, err.Error()) } diff --git a/internal/handler/grpc/order.go b/internal/handler/grpc/order.go index f159f72..af4c402 100644 --- a/internal/handler/grpc/order.go +++ b/internal/handler/grpc/order.go @@ -6,6 +6,8 @@ import ( "orderservice/internal/domain" "orderservice/internal/service" pb "orderservice/pkg/api/order" + + "github.com/google/uuid" ) type OrderHandler struct { @@ -41,7 +43,12 @@ func (h *OrderHandler) CreateOrder( } func (h *OrderHandler) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.GetOrderResponse, error) { - order, err := h.service.Get(ctx, req.GetId()) + parsedID, err := uuid.Parse(req.GetId()) + if err != nil { + return nil, domain.ErrInvalidID + } + + order, err := h.service.Get(ctx, parsedID) if err != nil { return nil, mapError(err) } @@ -53,7 +60,12 @@ func (h *OrderHandler) UpdateOrder( ctx context.Context, req *pb.UpdateOrderRequest, ) (*pb.UpdateOrderResponse, error) { - order, err := h.service.Update(ctx, req.GetId(), req.GetItem(), req.GetQuantity()) + parsedID, err := uuid.Parse(req.GetId()) + if err != nil { + return nil, domain.ErrInvalidID + } + + order, err := h.service.Update(ctx, parsedID, req.GetItem(), req.GetQuantity()) if err != nil { return nil, mapError(err) } @@ -65,7 +77,12 @@ func (h *OrderHandler) DeleteOrder( ctx context.Context, req *pb.DeleteOrderRequest, ) (*pb.DeleteOrderResponse, error) { - err := h.service.Delete(ctx, req.GetId()) + parsedID, err := uuid.Parse(req.GetId()) + if err != nil { + return nil, domain.ErrInvalidID + } + + err = h.service.Delete(ctx, parsedID) if err != nil { return nil, mapError(err) } diff --git a/internal/interceptor/logger.go b/internal/interceptor/logger.go index ad105b4..ca8c8ec 100644 --- a/internal/interceptor/logger.go +++ b/internal/interceptor/logger.go @@ -32,13 +32,13 @@ func (i *LoggerInterceptor) Unary() grpc.UnaryServerInterceptor { if err != nil { if st, ok := status.FromError(err); ok { - log.Printf("Error: %s, Code: %s, Duration: %v", + log.Printf("error: %s, code: %s, duration: %v", st.Message(), st.Code(), duration) } else { - log.Printf("Error: %v, Duration: %v", err, duration) + log.Printf("error: %v, duration: %v", err, duration) } } else { - log.Printf("Method %s completed in %v", info.FullMethod, duration) + log.Printf("method %s completed in %v", info.FullMethod, duration) } return resp, err @@ -61,10 +61,10 @@ func (i *LoggerInterceptor) Stream() grpc.StreamServerInterceptor { duration := time.Since(start) if err != nil { - log.Printf("Stream method %s failed: %v, Duration: %v", + log.Printf("stream method %s failed: %v, duration: %v", info.FullMethod, err, duration) } else { - log.Printf("Stream method %s completed in %v", + log.Printf("stream method %s completed in %v", info.FullMethod, duration) } diff --git a/internal/repository/inmemory/order.go b/internal/repository/inmemory/order.go index 37c5829..ef1d5be 100644 --- a/internal/repository/inmemory/order.go +++ b/internal/repository/inmemory/order.go @@ -5,6 +5,8 @@ import ( "sync" "orderservice/internal/domain" + + "github.com/google/uuid" ) type OrderRepository struct { @@ -34,7 +36,7 @@ func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error return nil } -func (r *OrderRepository) Get(ctx context.Context, id string) (*domain.Order, error) { +func (r *OrderRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) { if err := ctx.Err(); err != nil { return nil, err } @@ -42,7 +44,7 @@ func (r *OrderRepository) Get(ctx context.Context, id string) (*domain.Order, er r.mu.RLock() defer r.mu.RUnlock() - order, ok := r.orders[id] + order, ok := r.orders[id.String()] if !ok { return nil, domain.ErrOrderNotFound } @@ -66,7 +68,7 @@ func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error return nil } -func (r *OrderRepository) Delete(ctx context.Context, id string) error { +func (r *OrderRepository) Delete(ctx context.Context, id uuid.UUID) error { if err := ctx.Err(); err != nil { return err } @@ -74,11 +76,11 @@ func (r *OrderRepository) Delete(ctx context.Context, id string) error { r.mu.Lock() defer r.mu.Unlock() - if _, ok := r.orders[id]; !ok { + if _, ok := r.orders[id.String()]; !ok { return domain.ErrOrderNotFound } - delete(r.orders, id) + delete(r.orders, id.String()) return nil } diff --git a/internal/repository/order.go b/internal/repository/order.go index 48f0459..ee96418 100644 --- a/internal/repository/order.go +++ b/internal/repository/order.go @@ -4,12 +4,14 @@ import ( "context" "orderservice/internal/domain" + + "github.com/google/uuid" ) type OrderRepository interface { Create(ctx context.Context, order *domain.Order) error - Get(ctx context.Context, id string) (*domain.Order, error) + Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) Update(ctx context.Context, order *domain.Order) error - Delete(ctx context.Context, id string) error + Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context) ([]*domain.Order, error) } diff --git a/internal/repository/postgres/order.go b/internal/repository/postgres/order.go index 975833b..365675f 100644 --- a/internal/repository/postgres/order.go +++ b/internal/repository/postgres/order.go @@ -3,7 +3,6 @@ package postgres import ( "context" "database/sql" - _ "embed" "encoding/json" "errors" "fmt" @@ -12,24 +11,19 @@ import ( "orderservice/internal/domain" + "github.com/google/uuid" "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" // postgres driver "github.com/redis/go-redis/v9" ) -//go:embed schema.sql -var Schema string - const ( orderCachePrefix = "order:" cacheTTL = 5 * time.Minute - maxCacheRetries = 2 - cacheRetryDelay = 100 * time.Millisecond ) type OrderRepository struct { db *sqlx.DB - cache *redis.Client + redisClient *redis.Client cacheEnable bool } @@ -46,7 +40,7 @@ func NewOrderRepository(db *sqlx.DB, redisClient *redis.Client, config *Config) return &OrderRepository{ db: db, - cache: redisClient, + redisClient: redisClient, cacheEnable: config.CacheEnable, } } @@ -77,19 +71,19 @@ func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error if r.cacheEnable { if err := r.setCacheWithRetry(ctx, order); err != nil { - log.Printf("WARN: cache set error for order %s: %v", order.ID, err) + log.Printf("warn: cache set error for order %s: %v", order.ID, err) } } return nil } -func (r *OrderRepository) Get(ctx context.Context, id string) (*domain.Order, error) { +func (r *OrderRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) { if r.cacheEnable { - if order, err := r.getFromCache(ctx, id); err == nil { + if order, err := r.getFromCache(ctx, id.String()); err == nil { return order, nil } else if !errors.Is(err, redis.Nil) { - log.Printf("WARN: cache get error for order %s: %v", id, err) + log.Printf("warn: cache get error for order %s: %v", id, err) } } @@ -109,7 +103,7 @@ func (r *OrderRepository) Get(ctx context.Context, id string) (*domain.Order, er if r.cacheEnable { if err := r.setCacheWithRetry(ctx, &order); err != nil { - log.Printf("WARN: cache set error for order %s: %v", id, err) + log.Printf("warn: cache set error for order %s: %v", id, err) } } @@ -149,14 +143,14 @@ func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error if r.cacheEnable { if err := r.setCacheWithRetry(ctx, order); err != nil { - log.Printf("WARN: cache set error for order %s: %v", order.ID, err) + log.Printf("warn: cache set error for order %s: %v", order.ID, err) } } return nil } -func (r *OrderRepository) Delete(ctx context.Context, id string) error { +func (r *OrderRepository) Delete(ctx context.Context, id uuid.UUID) error { tx, err := r.db.BeginTxx(ctx, nil) if err != nil { return fmt.Errorf("begin transaction: %w", err) @@ -186,7 +180,7 @@ func (r *OrderRepository) Delete(ctx context.Context, id string) error { return fmt.Errorf("commit transaction: %w", err) } - r.invalidateCache(ctx, id) + r.invalidateCache(ctx, id.String()) return nil } @@ -206,14 +200,14 @@ func (r *OrderRepository) List(ctx context.Context) ([]*domain.Order, error) { } func (r *OrderRepository) getFromCache(ctx context.Context, id string) (*domain.Order, error) { - data, err := r.cache.Get(ctx, r.cacheKey(id)).Bytes() + data, err := r.redisClient.Get(ctx, r.cacheKey(id)).Bytes() if err != nil { return nil, err } var order domain.Order if err := json.Unmarshal(data, &order); err != nil { - r.cache.Del(ctx, r.cacheKey(id)) + r.redisClient.Del(ctx, r.cacheKey(id)) return nil, err } @@ -228,16 +222,7 @@ func (r *OrderRepository) setCacheWithRetry(ctx context.Context, order *domain.O key := r.cacheKey(order.ID.String()) - for i := range maxCacheRetries { - err = r.cache.Set(ctx, key, data, cacheTTL).Err() - if err == nil { - return nil - } - - if i < maxCacheRetries-1 { - time.Sleep(cacheRetryDelay) - } - } + err = r.redisClient.Set(ctx, key, data, cacheTTL).Err() return err } @@ -248,11 +233,11 @@ func (r *OrderRepository) invalidateCache(_ context.Context, id string) { } go func() { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), r.redisClient.Options().ReadTimeout) defer cancel() - if err := r.cache.Del(ctx, r.cacheKey(id)).Err(); err != nil { - log.Printf("WARN: cache invalidation failed for order %s: %v", id, err) + if err := r.redisClient.Del(ctx, r.cacheKey(id)).Err(); err != nil { + log.Printf("warn: cache invalidation failed for order %s: %v", id, err) } }() } diff --git a/internal/server/server.go b/internal/server/server.go index 2d8e380..b2b7ae6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "time" "orderservice/internal/config" "orderservice/internal/interceptor" @@ -19,12 +20,25 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" // postgres driver "github.com/redis/go-redis/v9" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" ) +const ( + httpReadTimeout = 10 * time.Second + httpWriteTimeout = 10 * time.Second + httpIdleTimeout = 60 * time.Second + redisRetryCount = 2 + redisMinRetryBackoff = 50 * time.Millisecond + redisMaxRetryBackoff = 200 * time.Millisecond + redisDialTimeout = 1 * time.Second + redisDialerRetries = 3 + redisTimeout = 2 * time.Second +) + type Server struct { grpcServer *grpc.Server config *config.Config @@ -62,28 +76,38 @@ func runHTTPHandler(s *Server, grpcServerEndpoint *string) error { mux.Handle("/healthz", httpHandlers.NewHealthHandler(s.db, s.redisDB)) mux.Handle("/", gwmux) - addr := fmt.Sprintf(":%d", s.config.HTTPPort) - return http.ListenAndServe(addr, mux) + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", s.config.HTTPPort), + Handler: mux, + ReadTimeout: httpReadTimeout, + WriteTimeout: httpWriteTimeout, + IdleTimeout: httpIdleTimeout, + } + + return srv.ListenAndServe() } func getDatabase(cfg config.Config) (*sqlx.DB, error) { - db, err := sqlx.Connect("postgres", cfg.BuildDsn()) + db, err := sqlx.Connect("postgres", cfg.BuildPostgresConnStr()) if err != nil { return nil, fmt.Errorf("connect to database: %w", err) } - _, err = db.Exec(orderPostgresRepo.Schema) - if err != nil { - return nil, fmt.Errorf("run schema: %w", err) - } - return db, nil } func getRedis(cfg config.Config) (*redis.Client, error) { conn, err := redis.ParseURL(cfg.RedisURI) client := redis.NewClient(&redis.Options{ - Addr: conn.Addr, + Addr: conn.Addr, + MaxRetries: redisRetryCount, + MinRetryBackoff: redisMinRetryBackoff, + MaxRetryBackoff: redisMaxRetryBackoff, + DialTimeout: redisDialTimeout, + DialerRetries: redisDialerRetries, + DialerRetryTimeout: redisDialTimeout, + ReadTimeout: redisTimeout, + WriteTimeout: redisTimeout, }) if err != nil { return nil, fmt.Errorf("parse Redis URI: %w", err) @@ -131,14 +155,14 @@ func (s *Server) Start() error { if s.config.EnableHTTPHandler { go func() { - log.Printf("Starting HTTP gateway on port %d", s.config.HTTPPort) + log.Printf("starting HTTP gateway on port %d", s.config.HTTPPort) if err := runHTTPHandler(s, &addr); err != nil { log.Printf("HTTP gateway failed: %v", err) } }() } - log.Printf("Starting gRPC server on port %d", s.config.GRPCPort) + log.Printf("starting gRPC server on port %d", s.config.GRPCPort) if err := s.grpcServer.Serve(lis); err != nil { return fmt.Errorf("failed to serve: %w", err) diff --git a/internal/service/order.go b/internal/service/order.go index 2075a01..065e61c 100644 --- a/internal/service/order.go +++ b/internal/service/order.go @@ -37,11 +37,11 @@ func (s *OrderService) Create(ctx context.Context, item string, quantity int32) return order, nil } -func (s *OrderService) Get(ctx context.Context, id string) (*domain.Order, error) { +func (s *OrderService) Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) { return s.repo.Get(ctx, id) } -func (s *OrderService) Update(ctx context.Context, id string, item string, quantity int32) (*domain.Order, error) { +func (s *OrderService) Update(ctx context.Context, id uuid.UUID, item string, quantity int32) (*domain.Order, error) { order, err := s.repo.Get(ctx, id) if err != nil { return nil, err @@ -61,7 +61,7 @@ func (s *OrderService) Update(ctx context.Context, id string, item string, quant return order, nil } -func (s *OrderService) Delete(ctx context.Context, id string) error { +func (s *OrderService) Delete(ctx context.Context, id uuid.UUID) error { return s.repo.Delete(ctx, id) }