Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ migrate:
setup-db: docker-down-v docker-up
@echo "⏳ Waiting for services to be ready..."
@sleep 10
@migrate
@./scripts/migrate.sh up

.PHONY: cleanup-db
cleanup-db:
@echo "🗄️ Cleaning up database (rolling back migrations)..."
@chmod +x scripts/migrate.sh
@./scripts/migrate.sh down

.PHONY: help
help:
Expand All @@ -90,6 +96,7 @@ help:
@echo " docker-down-v - Stop containers and remove volumes"
@echo " docker-up - Start services with Docker Compose"
@echo " migrate - Run database migrations"
@echo " cleanup-db - Rollback all database migrations"
@echo " setup-db - Clean start with Docker and run migrations"
@echo ""
@echo "⚠️ Note: protoc must be installed separately via system package manager"
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ make docker-up
# Запуск миграций базы данных
make migrate

# Откат всех миграций базы данных
make cleanup-db

# Полный перезапуск с Docker и миграции
make setup-db
```
Expand Down Expand Up @@ -252,6 +255,7 @@ curl -X DELETE http://localhost:8080/api/notes/{note_id} \
- `make docker-down-v` - остановка контейнеров и удаление volumes с БД
- `make docker-up` - запуск сервисов с Docker Compose
- `make migrate` - запуск миграций базы данных
- `make cleanup-db` - откат всех миграций базы данных
- `make setup-db` - полный перезапуск с Docker и запуск миграций
- `make help` - список всех целей

Expand Down
26 changes: 12 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,32 @@ require (
github.com/go-openapi/jsonpointer v0.22.3 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/swag/conv v0.25.3 // indirect
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
github.com/go-openapi/swag/loading v0.25.3 // indirect
github.com/go-openapi/swag/stringutils v0.25.3 // indirect
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-migrate/migrate/v4 v4.19.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
Expand All @@ -71,6 +69,6 @@ require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
433 changes: 23 additions & 410 deletions go.sum

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/app/lifecycle/shutdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func ShutdownHTTPServer(cfg *config.Config, server *http.Server) {
defer cancel()

if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Ошибка при graceful shutdown веб-сервера: %v", err)
log.Printf("Ошибка при graceful shutdown веб-сервера: %v", err)
}
}

Expand Down
81 changes: 57 additions & 24 deletions internal/app/lifecycle/startup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/rd2w/go-notes/internal/config"
"google.golang.org/grpc"
Expand All @@ -15,26 +18,41 @@ func StartHTTPServer(cfg *config.Config, server *http.Server) error {
// Канал для получения сигнала завершения
sigChan := make(chan struct{})

// Канал для получения ошибки при запуске сервера
errChan := make(chan error, 1)

// Канал для получения системного сигнала
sysSigChan := make(chan os.Signal, 1)
signal.Notify(sysSigChan, syscall.SIGINT, syscall.SIGTERM)

// Запускаем сервер в отдельной горутине
go func() {
log.Printf("Веб-сервер запущен на порту %s", server.Addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Ошибка при запуске веб-сервера: %v", err)
errChan <- err
return
}
close(sigChan)
}()

// Ждем сигнал завершения
WaitForShutdownSignal()

log.Println("Получен сигнал завершения, инициируем graceful shutdown...")

// Выполняем graceful shutdown
ShutdownHTTPServer(cfg, server)

// Ждем завершения работы сервера
<-sigChan
log.Println("Веб-сервер остановлен")
// Ждем сигнал завершения или ошибку при запуске
select {
case err := <-errChan:
return err
case <-sigChan:
// Сервер завершил работу по другим причинам
return nil
case <-sysSigChan:
// Получен сигнал завершения
log.Println("Получен сигнал завершения, инициируем graceful shutdown...")

// Выполняем graceful shutdown
ShutdownHTTPServer(cfg, server)

// Ждем завершения работы сервера
<-sigChan
log.Println("Веб-сервер остановлен")
}

return nil
}
Expand All @@ -44,26 +62,41 @@ func StartGRPCServer(cfg *config.Config, server *grpc.Server, lis net.Listener)
// Канал для получения сигнала завершения
sigChan := make(chan struct{})

// Канал для получения ошибки при запуске сервера
errChan := make(chan error, 1)

// Канал для получения системного сигнала
sysSigChan := make(chan os.Signal, 1)
signal.Notify(sysSigChan, syscall.SIGINT, syscall.SIGTERM)

// Запускаем gRPC сервер в отдельной горутине
go func() {
log.Printf("gRPC сервер запущен на порту %s", lis.Addr().String())
if err := server.Serve(lis); err != nil {
log.Fatalf("Ошибка при запуске gRPC сервера: %v", err)
errChan <- err
return
}
close(sigChan)
}()

// Ждем сигнал завершения
WaitForShutdownSignal()

log.Println("Получен сигнал завершения, инициируем graceful shutdown...")

// Выполняем graceful shutdown
ShutdownGRPCServer(server)

// Ждем завершения работы сервера
<-sigChan
log.Println("gRPC сервер остановлен")
// Ждем сигнал завершения или ошибку при запуске
select {
case err := <-errChan:
return err
case <-sigChan:
// Сервер завершил работу по другим причинам
return nil
case <-sysSigChan:
// Получен сигнал завершения
log.Println("Получен сигнал завершения, инициируем graceful shutdown...")

// Выполняем graceful shutdown
ShutdownGRPCServer(server)

// Ждем завершения работы сервера
<-sigChan
log.Println("gRPC сервер остановлен")
}

return nil
}
55 changes: 25 additions & 30 deletions internal/app/lifecycle/startup_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lifecycle

import (
"context"
"errors"
"net"
"net/http"
Expand Down Expand Up @@ -152,51 +153,45 @@ func TestHTTPServerAlreadyClosed(t *testing.T) {
},
}

// Создаем сервер на случайном порту
listener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("Не удалось создать слушатель: %v", err)
}

// Создаем HTTP сервер на случайном порту
server := &http.Server{
Addr: listener.Addr().String(),
Addr: ":0", // Используем случайный порт
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}),
}

// Закрываем слушатель до запуска сервера
_ = listener.Close()
// Запускаем сервер в отдельной горутине
listener, err := net.Listen("tcp", server.Addr)
if err != nil {
t.Fatalf("Не удалось создать слушатель: %v", err)
}

// Создаем канал для перехвата сигнала завершения
done := make(chan bool, 1)
serverStarted := make(chan bool, 1)
go func() {
// Ожидаем ошибку при запуске сервера
err := server.ListenAndServe()
if err != nil && (errors.Is(err, http.ErrServerClosed) || err.Error() != "") {
done <- true
serverStarted <- true
// Пытаемся запустить сервер, но он будет закрыт до завершения
err := server.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
t.Logf("Ошибка при работе сервера: %v", err)
}
}()

// Запускаем сервер в отдельной горутине
go func() {
time.Sleep(10 * time.Millisecond)
sendShutdownSignal()
}()
// Ждем, пока сервер начнет запускаться
<-serverStarted
time.Sleep(10 * time.Millisecond)

// Закрываем сервер принудительно
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_ = server.Shutdown(ctx)

// Вызываем StartHTTPServer, но сервер уже закрыт
err = StartHTTPServer(cfg, server)
if err != nil {
// Ошибка ожидаема, так как сервер не может запуститься
// Ошибка ожидаема, так как сервер уже закрыт
// Проверяем, что функция завершается без паники
t.Logf("Ожидаемая ошибка при запуске сервера: %v", err)
}

// Ждем завершения сервера
select {
case <-done:
// Сервер завершился корректно
case <-time.After(2 * time.Second):
t.Error("Таймаут ожидания завершения сервера")
t.Logf("Ожидаемая ситуация - сервер уже закрыт: %v", err)
}
}
84 changes: 84 additions & 0 deletions internal/repository/postgres/base_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package postgres

import (
"context"
"time"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)

// BaseRepository содержит общую логику для всех репозиториев
type BaseRepository struct {
db *pgxpool.Pool
tx pgx.Tx // текущая транзакция (если есть)
timeout time.Duration
}

// NewBaseRepository создает новый экземпляр базового репозитория
func NewBaseRepository(db *pgxpool.Pool, timeout time.Duration) *BaseRepository {
if timeout <= 0 {
timeout = 5 * time.Second // значение по умолчанию
}
return &BaseRepository{
db: db,
timeout: timeout,
}
}

// WithContext возвращает контекст с таймаутом
func (r *BaseRepository) WithContext(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, r.timeout)
}

// SetTransaction устанавливает транзакцию для репозитория
func (r *BaseRepository) SetTransaction(tx pgx.Tx) {
r.tx = tx
}

// ClearTransaction убирает транзакцию
func (r *BaseRepository) ClearTransaction() {
r.tx = nil
}

// GetDB возвращает пул подключений или транзакцию, если она установлена
func (r *BaseRepository) GetDB() interface{} {
if r.tx != nil {
return r.tx
}
return r.db
}

// Exec выполняет SQL-запрос и возвращает результат
func (r *BaseRepository) Exec(ctx context.Context, sql string, args ...interface{}) (int64, error) {
var commandTag pgconn.CommandTag
var err error

if r.tx != nil {
commandTag, err = r.tx.Exec(ctx, sql, args...)
} else {
commandTag, err = r.db.Exec(ctx, sql, args...)
}

if err != nil {
return 0, err
}
return commandTag.RowsAffected(), nil
}

// QueryRow выполняет запрос и возвращает одну строку
func (r *BaseRepository) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
if r.tx != nil {
return r.tx.QueryRow(ctx, sql, args...)
}
return r.db.QueryRow(ctx, sql, args...)
}

// Query выполняет запрос и возвращает несколько строк
func (r *BaseRepository) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
if r.tx != nil {
return r.tx.Query(ctx, sql, args...)
}
return r.db.Query(ctx, sql, args...)
}
Loading