diff --git a/.env.template b/.env.template index 52d9bde..07506a0 100644 --- a/.env.template +++ b/.env.template @@ -6,3 +6,9 @@ GRPC_ENABLE_REFLECTION=false HTTP_HANDLER_ENABLE=false HTTP_PORT=8080 LOG_LEVEL=info +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USERNAME=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DATABASE=postgres +REDIS_URI=redis://localhost:6379 diff --git a/cmd/server/main.go b/cmd/server/main.go index e056e68..0deee27 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -26,7 +26,7 @@ func main() { }() quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") diff --git a/go.mod b/go.mod index 0d0b86d..a685be3 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,20 @@ toolchain go1.24.9 require ( github.com/google/uuid v1.6.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 + github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/redis/go-redis/v9 v9.16.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 ) require ( - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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 golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.34.0 // indirect diff --git a/go.sum b/go.sum index 067ee0c..d2d9396 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,20 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/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/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= @@ -10,8 +23,22 @@ 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/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= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/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/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= 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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -30,16 +57,12 @@ golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= @@ -49,3 +72,5 @@ google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zt google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= diff --git a/internal/config/config.go b/internal/config/config.go index a17eadb..67ccdfa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "log" "os" "strconv" @@ -14,6 +15,12 @@ type Config struct { EnableHTTPHandler bool HTTPPort int LogLevel string + DBHost string + DBPort int + DBUser string + DBPassword string + DBName string + RedisURI string } func Load() (*Config, error) { @@ -25,6 +32,12 @@ func Load() (*Config, error) { EnableHTTPHandler: mustGetBool("HTTP_HANDLER_ENABLE", false), HTTPPort: mustGetInt("HTTP_PORT", 8080), //nolint:mnd // false-positive LogLevel: getEnv("LOG_LEVEL", "info"), + DBHost: getEnv("POSTGRES_HOST", "localhost"), + DBPort: mustGetInt("POSTGRES_PORT", 5432), + DBUser: getEnv("POSTGRES_USERNAME", "postgres"), + DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), + DBName: getEnv("POSTGRES_DATABASE", "postgres"), + RedisURI: getEnv("REDIS_URI", "redis://localhost:6379"), }, nil } @@ -52,3 +65,8 @@ func mustGetBool(key string, def bool) bool { } return b } + +func (c Config) BuildDsn() 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) +} diff --git a/internal/domain/order.go b/internal/domain/order.go index 23fc535..0157d8f 100644 --- a/internal/domain/order.go +++ b/internal/domain/order.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "strings" + + "github.com/google/uuid" ) var ( @@ -13,9 +15,9 @@ var ( ) type Order struct { - ID string - Item string - Quantity int32 + ID uuid.UUID `db:"id" json:"id"` + Item string `db:"item" json:"item"` + Quantity int32 `db:"quantity" json:"quantity"` } func (o *Order) Validate() error { @@ -25,7 +27,7 @@ func (o *Order) Validate() error { if o.Quantity <= 0 { return fmt.Errorf("%w: quantity must be positive", ErrInvalidOrderData) } - if o.ID == "" { + if o.ID.String() == "" { return fmt.Errorf("%w: ID cannot be empty", ErrInvalidOrderData) } return nil diff --git a/internal/handler/grpc/errors.go b/internal/handler/grpc/errors.go index aa7ae1e..071fc13 100644 --- a/internal/handler/grpc/errors.go +++ b/internal/handler/grpc/errors.go @@ -2,6 +2,7 @@ package handler import ( "errors" + "log" "orderservice/internal/domain" @@ -20,5 +21,6 @@ func mapError(err error) error { return status.Error(codes.InvalidArgument, err.Error()) } + log.Printf("internal server error: %v", err) return status.Error(codes.Internal, "internal server error") } diff --git a/internal/handler/grpc/order.go b/internal/handler/grpc/order.go index 53e3fb0..f159f72 100644 --- a/internal/handler/grpc/order.go +++ b/internal/handler/grpc/order.go @@ -22,7 +22,7 @@ func NewOrderHandler(service *service.OrderService) *OrderHandler { func mapDomainStructToHandler(order *domain.Order) *pb.Order { return &pb.Order{ - Id: order.ID, + Id: order.ID.String(), Item: order.Item, Quantity: order.Quantity, } @@ -37,7 +37,7 @@ func (h *OrderHandler) CreateOrder( return nil, mapError(err) } - return &pb.CreateOrderResponse{Id: order.ID}, nil + return &pb.CreateOrderResponse{Id: order.ID.String()}, nil } func (h *OrderHandler) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.GetOrderResponse, error) { diff --git a/internal/repository/inmemory/order.go b/internal/repository/inmemory/order.go index f3da9e2..37c5829 100644 --- a/internal/repository/inmemory/order.go +++ b/internal/repository/inmemory/order.go @@ -26,10 +26,10 @@ func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error r.mu.Lock() defer r.mu.Unlock() - if _, ok := r.orders[order.ID]; ok { + if _, ok := r.orders[order.ID.String()]; ok { return domain.ErrOrderAlreadyExist } - r.orders[order.ID] = order + r.orders[order.ID.String()] = order return nil } @@ -58,10 +58,10 @@ func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error r.mu.Lock() defer r.mu.Unlock() - if _, ok := r.orders[order.ID]; !ok { + if _, ok := r.orders[order.ID.String()]; !ok { return domain.ErrOrderNotFound } - r.orders[order.ID] = order + r.orders[order.ID.String()] = order return nil } diff --git a/internal/repository/postgres/order.go b/internal/repository/postgres/order.go new file mode 100644 index 0000000..975833b --- /dev/null +++ b/internal/repository/postgres/order.go @@ -0,0 +1,258 @@ +package postgres + +import ( + "context" + "database/sql" + _ "embed" + "encoding/json" + "errors" + "fmt" + "log" + "time" + + "orderservice/internal/domain" + + "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 + cacheEnable bool +} + +type Config struct { + CacheEnable bool +} + +func NewOrderRepository(db *sqlx.DB, redisClient *redis.Client, config *Config) *OrderRepository { + if config == nil { + config = &Config{ + CacheEnable: true, + } + } + + return &OrderRepository{ + db: db, + cache: redisClient, + cacheEnable: config.CacheEnable, + } +} + +func (r *OrderRepository) cacheKey(id string) string { + return orderCachePrefix + id +} + +func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error { + tx, err := r.db.BeginTxx(ctx, nil) + if err != nil { + return fmt.Errorf("begin transaction: %w", err) + } + defer tx.Rollback() + + query := ` + insert into orders (id, item, quantity) + values (:id, :item, :quantity) + ` + + if _, err := tx.NamedExecContext(ctx, query, order); err != nil { + return fmt.Errorf("create order: %w", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit transaction: %w", err) + } + + if r.cacheEnable { + if err := r.setCacheWithRetry(ctx, order); err != nil { + 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) { + if r.cacheEnable { + if order, err := r.getFromCache(ctx, id); err == nil { + return order, nil + } else if !errors.Is(err, redis.Nil) { + log.Printf("WARN: cache get error for order %s: %v", id, err) + } + } + + const query = ` + select id, item, quantity + from orders + where id = $1 + ` + + var order domain.Order + if err := r.db.GetContext(ctx, &order, query, id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, domain.ErrOrderNotFound + } + return nil, fmt.Errorf("get order by id: %w", err) + } + + if r.cacheEnable { + if err := r.setCacheWithRetry(ctx, &order); err != nil { + log.Printf("WARN: cache set error for order %s: %v", id, err) + } + } + + return &order, nil +} + +func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error { + tx, err := r.db.BeginTxx(ctx, nil) + if err != nil { + return fmt.Errorf("begin transaction: %w", err) + } + defer tx.Rollback() + + query := ` + update orders + set item = :item, quantity = :quantity + where id = :id + ` + + result, err := tx.NamedExecContext(ctx, query, order) + if err != nil { + return fmt.Errorf("update order: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("get rows affected: %w", err) + } + + if rowsAffected == 0 { + return domain.ErrOrderNotFound + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit transaction: %w", err) + } + + if r.cacheEnable { + if err := r.setCacheWithRetry(ctx, order); err != nil { + 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 { + tx, err := r.db.BeginTxx(ctx, nil) + if err != nil { + return fmt.Errorf("begin transaction: %w", err) + } + defer tx.Rollback() + + const query = ` + delete from orders + where id = $1 + ` + + result, err := tx.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("delete order: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("get rows affected: %w", err) + } + + if rowsAffected == 0 { + return domain.ErrOrderNotFound + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit transaction: %w", err) + } + + r.invalidateCache(ctx, id) + return nil +} + +func (r *OrderRepository) List(ctx context.Context) ([]*domain.Order, error) { + const query = ` + select id, item, quantity + from orders + order by id + ` + + var orders []*domain.Order + if err := r.db.SelectContext(ctx, &orders, query); err != nil { + return nil, fmt.Errorf("list orders: %w", err) + } + + return orders, nil +} + +func (r *OrderRepository) getFromCache(ctx context.Context, id string) (*domain.Order, error) { + data, err := r.cache.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)) + return nil, err + } + + return &order, nil +} + +func (r *OrderRepository) setCacheWithRetry(ctx context.Context, order *domain.Order) error { + data, err := json.Marshal(order) + if err != nil { + return err + } + + 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) + } + } + + return err +} + +func (r *OrderRepository) invalidateCache(_ context.Context, id string) { + if !r.cacheEnable { + return + } + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + 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) + } + }() +} diff --git a/internal/repository/postgres/schema.sql b/internal/repository/postgres/schema.sql new file mode 100644 index 0000000..bd3e31a --- /dev/null +++ b/internal/repository/postgres/schema.sql @@ -0,0 +1,5 @@ +create table if not exists orders ( + id uuid primary key, + item varchar(500) not null, + quantity integer not null check (quantity > 0) +); diff --git a/internal/server/server.go b/internal/server/server.go index aa27da1..1cfbbac 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,13 +10,15 @@ import ( "orderservice/internal/config" "orderservice/internal/interceptor" - orderGrpcHandler "orderservice/internal/handler/grpc" - orderInMemory "orderservice/internal/repository/inmemory" + grpcHandlers "orderservice/internal/handler/grpc" + orderPostgresRepo "orderservice/internal/repository/postgres" "orderservice/internal/service" pb "orderservice/pkg/api/order" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" @@ -25,6 +27,8 @@ import ( type Server struct { grpcServer *grpc.Server config *config.Config + db *sqlx.DB + redisDB *redis.Client } func New(cfg *config.Config) *Server { @@ -46,21 +50,64 @@ func runHTTPHandler(s *Server, grpcServerEndpoint *string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() - mux := runtime.NewServeMux() + gwmux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} - err := pb.RegisterOrderServiceHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts) + err := pb.RegisterOrderServiceHandlerFromEndpoint(ctx, gwmux, *grpcServerEndpoint, opts) if err != nil { return err } addr := fmt.Sprintf(":%d", s.config.HTTPPort) - return http.ListenAndServe(addr, mux) + return http.ListenAndServe(addr, gwmux) +} + +func getDatabase(cfg config.Config) (*sqlx.DB, error) { + db, err := sqlx.Connect("postgres", cfg.BuildDsn()) + 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, + }) + if err != nil { + return nil, fmt.Errorf("parse Redis URI: %w", err) + } + + _, err = client.Ping(context.Background()).Result() + if err != nil { + return nil, fmt.Errorf("connect to Redis server: %w", err) + } + + return client, nil } func (s *Server) RegisterServices() { - repo := orderInMemory.NewOrderRepository() - orderService := service.NewOrderService(repo) - orderHandler := orderGrpcHandler.NewOrderHandler(orderService) + db, err := getDatabase(*s.config) + if err != nil { + log.Print(err) + } + s.db = db + + redisDB, err := getRedis(*s.config) + if err != nil { + log.Print(err) + } + s.redisDB = redisDB + + orderRepo := orderPostgresRepo.NewOrderRepository(db, redisDB, &orderPostgresRepo.Config{CacheEnable: true}) + orderService := service.NewOrderService(orderRepo) + orderHandler := grpcHandlers.NewOrderHandler(orderService) pb.RegisterOrderServiceServer(s.grpcServer, orderHandler) diff --git a/internal/service/order.go b/internal/service/order.go index 10dede4..2075a01 100644 --- a/internal/service/order.go +++ b/internal/service/order.go @@ -21,7 +21,7 @@ func NewOrderService(repo repository.OrderRepository) *OrderService { func (s *OrderService) Create(ctx context.Context, item string, quantity int32) (*domain.Order, error) { order := &domain.Order{ - ID: uuid.NewString(), + ID: uuid.New(), Item: item, Quantity: quantity, }