Compare commits

..

5 Commits

Author SHA1 Message Date
ITQ a6b0668cbd chore(): small improvements 2026-01-13 09:34:48 +03:00
ITQ f4030b5cd2 chore: improved validation 2025-11-16 16:12:46 +03:00
ITQ b2e45b6f47 chore: improved update logic 2025-11-16 14:43:43 +03:00
ITQ c519941c5a feat: added migrations 2025-11-13 16:10:28 +03:00
ITQ c3def9dc26 chore: project refactor 2025-11-13 16:09:56 +03:00
20 changed files with 387 additions and 106 deletions
+51
View File
@@ -0,0 +1,51 @@
ARG GOARCH=amd64
ARG GOOS=linux
ARG CGO_ENABLED=0
ARG VERSION=1.0.0
ARG BUILD_TIME=unknown
# Stage 1: Build
FROM docker.io/golang:1.24-alpine AS build
ARG GOOS
ARG GOARCH
ARG CGO_ENABLED
ARG VERSION
ARG BUILD_TIME
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=${CGO_ENABLED} \
GOOS=${GOOS} \
GOARCH=${GOARCH} \
go build -trimpath \
-ldflags="-s -w -X 'main.Version=${VERSION}' -X 'main.BuildTime=${BUILD_TIME}'" \
-o /bin/app ./cmd/migrate
# Stage 2: Runtime
FROM docker.io/alpine:3.22 AS runtime
ARG VERSION
ARG BUILD_TIME
RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY --from=build /bin/app /app/bin
RUN chown app:app /app/bin && chmod +x /app/bin
USER app
LABEL org.opencontainers.image.version=${VERSION}
LABEL org.opencontainers.image.created=${BUILD_TIME}
ENTRYPOINT ["/app/bin"]
CMD [""]
+18 -2
View File
@@ -1,9 +1,10 @@
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOBUILD=$(GOCMD) build -trimpath -ldflags="-s -w"
GOTEST=$(GOCMD) test
GODOWNLOAD=$(GOCMD) mod download
BINARY_NAME=orderservice
MIGRATE_BINARY_NAME=orderservice-migrate
BINARY_DIR=bin
# Protobuf parameters
@@ -12,7 +13,7 @@ PROTO_DIR=api/proto
PROTO_FILE=$(PROTO_DIR)/order.proto
PROTO_OUT=.
.PHONY: install i generate gen protoc test build run lint fmt format clean help
.PHONY: install i generate gen generate-gw test build run migrate lint fmt format clean help
install:
$(GODOWNLOAD)
@@ -38,9 +39,21 @@ build:
$(GOBUILD) -o ./$(BINARY_DIR)/$(BINARY_NAME) ./cmd/server
chmod +x ./$(BINARY_DIR)/$(BINARY_NAME)
build-migrate:
$(GOBUILD) -o ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) ./cmd/migrate
chmod +x ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME)
run: build
./$(BINARY_DIR)/$(BINARY_NAME)
migrate: build-migrate
@cmd=$(word 2,$(MAKECMDGOALS)); \
if [ -z "$$cmd" ]; then \
echo "Usage: make migrate <command>"; \
exit 1; \
fi; \
./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) -cmd $$cmd
lint:
golangci-lint run -c .golangci.yaml ./...
@@ -68,3 +81,6 @@ help:
@echo " clean - Clean build artifacts"
.DEFAULT_GOAL := help
%:
@:
+78
View File
@@ -0,0 +1,78 @@
package main
import (
"database/sql"
"embed"
"errors"
"flag"
"log"
"orderservice/internal/config"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func main() {
var cmd string
flag.StringVar(&cmd, "cmd", "up", "migration command: up|down|force|version")
flag.Parse()
cfg, err := config.Load()
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
db, err := sql.Open("postgres", cfg.BuildPostgresDSN())
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
drv, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
log.Printf("postgres driver: %v", err)
return
}
src, err := iofs.New(migrationsFS, "migrations")
if err != nil {
log.Printf("iofs source: %v", err)
return
}
m, err := migrate.NewWithInstance("iofs", src, "postgres", drv)
if err != nil {
log.Printf("migrate NewWithInstance: %v", err)
return
}
switch cmd {
case "up":
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
log.Printf("m.Up failed: %v", err)
return
}
log.Println("migrations applied (up)")
case "down":
if err := m.Steps(-1); err != nil {
log.Printf("m.Steps(-1) failed: %v", err)
return
}
log.Println("stepped down 1 migration")
case "version":
v, dirty, verr := m.Version()
if verr != nil {
log.Printf("version: %v", verr)
return
}
log.Printf("version: %d dirty: %v\n", v, dirty)
default:
log.Printf("unknown cmd: %s", cmd)
}
}
@@ -0,0 +1 @@
drop table if exists orders;
+4 -4
View File
@@ -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")
}
+22
View File
@@ -6,6 +6,10 @@ services:
context: .
dockerfile: Containerfile
depends_on:
migrate:
restart: false
condition: service_completed_successfully
required: true
postgres:
restart: false
condition: service_healthy
@@ -36,6 +40,24 @@ services:
restart: unless-stopped
shm_size: 4mb
migrate:
build:
context: .
dockerfile: Containerfile.migrate
depends_on:
postgres:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/core/.env.template
required: true
- path: ./infrastructure/core/.env
networks:
- default
restart: no
shm_size: 4mb
postgres:
image: docker.io/postgres:17-alpine
configs:
+11 -2
View File
@@ -5,6 +5,8 @@ go 1.24.0
toolchain go1.24.9
require (
github.com/go-playground/validator/v10 v10.28.0
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,11 +20,18 @@ require (
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // 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/leodido/go-urn v1.4.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
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
+69 -4
View File
@@ -1,20 +1,56 @@
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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
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 +59,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=
@@ -31,16 +72,36 @@ 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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=
@@ -53,10 +114,12 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
@@ -74,3 +137,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=
+8 -2
View File
@@ -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)
}
+9
View File
@@ -0,0 +1,9 @@
package domain
import (
"errors"
)
var (
ErrInvalidID = errors.New("invalid uuid")
)
+22 -14
View File
@@ -3,8 +3,8 @@ package domain
import (
"errors"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -15,20 +15,28 @@ var (
)
type Order struct {
ID uuid.UUID `db:"id" json:"id"`
Item string `db:"item" json:"item"`
Quantity int32 `db:"quantity" json:"quantity"`
ID uuid.UUID `db:"id" json:"id" validate:"required"`
Item string `db:"item" json:"item" validate:"required"`
Quantity int32 `db:"quantity" json:"quantity" validate:"required,gt=0"`
}
func NewOrder(id uuid.UUID, item string, quantity int32) (*Order, error) {
order := &Order{
ID: id,
Item: item,
Quantity: quantity,
}
err := order.Validate()
if err != nil {
return nil, err
}
return order, nil
}
func (o *Order) Validate() error {
if strings.TrimSpace(o.Item) == "" {
return fmt.Errorf("%w: item cannot be empty", ErrInvalidOrderData)
}
if o.Quantity <= 0 {
return fmt.Errorf("%w: quantity must be positive", ErrInvalidOrderData)
}
if o.ID.String() == "" {
return fmt.Errorf("%w: ID cannot be empty", ErrInvalidOrderData)
}
return nil
validate := validator.New()
return fmt.Errorf("%w: %w", ErrInvalidOrderData, validate.Struct(o))
}
+1 -1
View File
@@ -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())
}
+20 -3
View File
@@ -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, mapError(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)
}
+5 -5
View File
@@ -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)
}
+7 -5
View File
@@ -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
}
+4 -2
View File
@@ -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)
}
+17 -32
View File
@@ -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)
}
}()
}
+35 -11
View File
@@ -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)
+5 -19
View File
@@ -20,13 +20,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.New(),
Item: item,
Quantity: quantity,
}
err := order.Validate()
order, err := domain.NewOrder(uuid.New(), item, quantity)
if err != nil {
return nil, err
}
@@ -37,20 +31,12 @@ 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) {
order, err := s.repo.Get(ctx, id)
if err != nil {
return nil, err
}
order.Item = item
order.Quantity = quantity
err = order.Validate()
func (s *OrderService) Update(ctx context.Context, id uuid.UUID, item string, quantity int32) (*domain.Order, error) {
order, err := domain.NewOrder(id, item, quantity)
if err != nil {
return nil, err
}
@@ -61,7 +47,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)
}