From 6250623fc92365c21a9c3cbc664a61aa42514c81 Mon Sep 17 00:00:00 2001 From: RD2W Date: Tue, 4 Nov 2025 19:51:36 +0300 Subject: [PATCH 1/2] feat: implement graceful shutdown with context and OS signals --- cmd/notes/main.go | 28 ++++++++----- internal/logger/logger.go | 9 +++-- internal/logger/logger_test.go | 69 ++++++++++++++++---------------- internal/service/service.go | 13 +++--- internal/service/service_test.go | 63 +++++++++++++++-------------- 5 files changed, 98 insertions(+), 84 deletions(-) diff --git a/cmd/notes/main.go b/cmd/notes/main.go index 5150506..5112876 100644 --- a/cmd/notes/main.go +++ b/cmd/notes/main.go @@ -1,8 +1,12 @@ package main import ( + "context" "fmt" "log" + "os" + "os/signal" + "syscall" "time" "github.com/rd2w/go-notes/internal/logger" @@ -41,23 +45,29 @@ const ( func main() { log.Println(AppStartMsg) - // Создаем канал для завершения - done := make(chan struct{}) + + // Создаем контекст с отменой для graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) + + // Обработка сигналов ОС + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) // Инициализируем компоненты repo := repository.NewRepository() - svc := service.NewService(repo, done, DataGenInterval) - newLogger := logger.NewLogger(repo, done, LoggerInterval) + svc := service.NewService(repo, ctx, DataGenInterval) + newLogger := logger.NewLogger(repo, ctx, LoggerInterval) // Запускаем горутины go newLogger.Start() // Логгер мониторит изменения - svc.Start() // Сервис запускает генерацию и сохранение данных + go svc.Start() // Сервис запускает генерацию и сохранение данных - // Ждем некоторое время для демонстрации работы - time.Sleep(AppRunDuration) + // Ждем сигнал завершения + <-sigChan + log.Println("Получен сигнал завершения, инициируем graceful shutdown...") - // Сигнал завершения всем горутинам - close(done) + // Отменяем контекст для завершения всех горутин + cancel() // Даем время на корректное завершение time.Sleep(GracefulShutdownDelay) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 739d94a..99a9a0c 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,6 +1,7 @@ package logger import ( + "context" "log" "time" @@ -10,15 +11,15 @@ import ( // Logger отвечает за логирование изменений в данных type Logger struct { repo *repository.Repository - done <-chan struct{} + ctx context.Context interval time.Duration } // NewLogger создает новый экземпляр логгера -func NewLogger(repo *repository.Repository, done <-chan struct{}, interval time.Duration) *Logger { +func NewLogger(repo *repository.Repository, ctx context.Context, interval time.Duration) *Logger { return &Logger{ repo: repo, - done: done, + ctx: ctx, interval: interval, } } @@ -49,7 +50,7 @@ func (l *Logger) Start() { lastNoteCount = currentNoteCount } - case <-l.done: + case <-l.ctx.Done(): log.Println("Логгер: завершение работы") return } diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 914a6e0..1d0fd25 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -2,6 +2,7 @@ package logger import ( "bytes" + "context" "log" "strings" "sync" @@ -39,11 +40,11 @@ func TestLogger_IntegrationWithService(t *testing.T) { defer log.SetOutput(oldOutput) // Создаем компоненты как в main() - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) repo := repository.NewRepository() - svc := service.NewService(repo, done, 50*time.Millisecond) - logger := NewLogger(repo, done, 30*time.Millisecond) + svc := service.NewService(repo, ctx, 50*time.Millisecond) + logger := NewLogger(repo, ctx, 30*time.Millisecond) // Запускаем компоненты go logger.Start() @@ -52,8 +53,8 @@ func TestLogger_IntegrationWithService(t *testing.T) { // Ждем достаточно времени для обработки нескольких итераций time.Sleep(100 * time.Millisecond) - // Закрываем канал после проверки - close(done) + // Завершаем работу через контекст + cancel() // Ждем немного, чтобы логгер успел завершить работу и вывести сообщения time.Sleep(10 * time.Millisecond) @@ -72,18 +73,18 @@ func TestLogger_IntegrationWithService(t *testing.T) { t.Logf("Вывод логгера:\n%s", output) } -func TestLogger_StopWithDoneChannel(t *testing.T) { +func TestLogger_StopWithContext(t *testing.T) { var buf safeBuffer oldOutput := log.Writer() log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) repo := repository.NewRepository() // Увеличиваем интервал логгера чтобы он реже проверял - logger := NewLogger(repo, done, 100*time.Millisecond) + logger := NewLogger(repo, ctx, 100*time.Millisecond) // Запускаем компоненты go logger.Start() @@ -103,7 +104,7 @@ func TestLogger_StopWithDoneChannel(t *testing.T) { time.Sleep(20 * time.Millisecond) // Теперь останавливаем ДО того как логгер успеет проверить - close(done) + cancel() // Даем время на завершение time.Sleep(50 * time.Millisecond) @@ -115,7 +116,7 @@ func TestLogger_StopWithDoneChannel(t *testing.T) { // В этом тесте мы специально останавливаем логгер ДО того как он проверит заметки // Поэтому он может не успеть залогировать заметки - это нормальное поведение - t.Logf("Тест завершен: логгер корректно остановился по сигналу done") + t.Logf("Тест завершен: логгер корректно остановился по сигналу контекста") } func TestLogger_MultipleNoteGeneration(t *testing.T) { @@ -124,11 +125,11 @@ func TestLogger_MultipleNoteGeneration(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - logger := NewLogger(repo, done, 40*time.Millisecond) + logger := NewLogger(repo, ctx, 40*time.Millisecond) // Запускаем компоненты go logger.Start() @@ -185,11 +186,11 @@ func TestLogger_NoNotesScenario(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - logger := NewLogger(repo, done, 30*time.Millisecond) + logger := NewLogger(repo, ctx, 30*time.Millisecond) // Запускаем только логгер, но не отправляем заметки go logger.Start() @@ -210,12 +211,12 @@ func TestLogger_ConcurrentAccess(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() // Увеличиваем интервал для стабильности - logger := NewLogger(repo, done, 30*time.Millisecond) + logger := NewLogger(repo, ctx, 30*time.Millisecond) // Запускаем компоненты go logger.Start() @@ -259,11 +260,11 @@ func TestLogger_TimeFormatConsistency(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - logger := NewLogger(repo, done, 50*time.Millisecond) + logger := NewLogger(repo, ctx, 50*time.Millisecond) go logger.Start() @@ -300,12 +301,12 @@ func TestLogger_SimpleCase(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() // Очень короткий интервал для быстрого обнаружения - logger := NewLogger(repo, done, 10*time.Millisecond) + logger := NewLogger(repo, ctx, 10*time.Millisecond) go logger.Start() @@ -338,12 +339,12 @@ func TestLogger_SeesNotesBeforeStop(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) repo := repository.NewRepository() // Очень короткий интервал для быстрого обнаружения - logger := NewLogger(repo, done, 10*time.Millisecond) + logger := NewLogger(repo, ctx, 10*time.Millisecond) // Запускаем компоненты go logger.Start() @@ -362,7 +363,7 @@ func TestLogger_SeesNotesBeforeStop(t *testing.T) { time.Sleep(30 * time.Millisecond) // Теперь останавливаем - close(done) + cancel() // Даем время на завершение time.Sleep(20 * time.Millisecond) @@ -384,13 +385,13 @@ func TestLogger_ImmediateStop(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) repo := repository.NewRepository() - logger := NewLogger(repo, done, 10*time.Millisecond) + logger := NewLogger(repo, ctx, 10*time.Millisecond) // Останавливаем СРАЗУ ЖЕ - close(done) + cancel() // Запускаем компоненты после остановки go logger.Start() @@ -416,13 +417,13 @@ func TestLogger_GracefulStop(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(oldOutput) - done := make(chan struct{}) - defer close(done) // На этот раз используем defer + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // На этот раз используем defer repo := repository.NewRepository() // Нормальный интервал - logger := NewLogger(repo, done, 50*time.Millisecond) + logger := NewLogger(repo, ctx, 50*time.Millisecond) // Запускаем компоненты go logger.Start() diff --git a/internal/service/service.go b/internal/service/service.go index 5745fbf..86d8b04 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,6 +1,7 @@ package service import ( + "context" "fmt" "log" "time" @@ -12,15 +13,15 @@ import ( // Service содержит бизнес-логику приложения type Service struct { repo *repository.Repository - done <-chan struct{} + ctx context.Context interval time.Duration } // NewService создает новый экземпляр сервиса -func NewService(repo *repository.Repository, done <-chan struct{}, interval time.Duration) *Service { +func NewService(repo *repository.Repository, ctx context.Context, interval time.Duration) *Service { return &Service{ repo: repo, - done: done, + ctx: ctx, interval: interval, } } @@ -55,7 +56,7 @@ func (s *Service) startDataGeneration(entityChan chan<- repository.Entity) { select { case entityChan <- note: log.Printf("Сервис: создана заметка %d", noteCounter) - case <-s.done: + case <-s.ctx.Done(): log.Println("Сервис: завершение генерации данных") return } @@ -65,7 +66,7 @@ func (s *Service) startDataGeneration(entityChan chan<- repository.Entity) { log.Println("Сервис: генерация тестовых данных завершена") return } - case <-s.done: + case <-s.ctx.Done(): log.Println("Сервис: завершение работы генерации по сигналу") return } @@ -81,7 +82,7 @@ func (s *Service) startDataSaving(entityChan <-chan repository.Entity) { // Вызываем синхронный метод сохранения в репозитории s.repo.Save(entity) log.Printf("Сервис: сохранена сущность %s", entity.GetID()) - case <-s.done: + case <-s.ctx.Done(): log.Println("Сервис: завершение сохранения данных") return } diff --git a/internal/service/service_test.go b/internal/service/service_test.go index 8f333bc..b22a5d0 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -2,6 +2,7 @@ package service import ( "bytes" + "context" "fmt" "log" "sync" @@ -31,16 +32,16 @@ func (s *safeBuffer) String() string { return s.buf.String() } -// TestService_StopWithDoneChannel тестирует остановку генерации через done канал -func TestService_StopWithDoneChannel(t *testing.T) { +// TestService_StopWithContext тестирует остановку генерации через контекст +func TestService_StopWithContext(t *testing.T) { var buf safeBuffer log.SetOutput(&buf) defer log.SetOutput(log.Writer()) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) repo := repository.NewRepository() - service := NewService(repo, done, 20*time.Millisecond) + service := NewService(repo, ctx, 20*time.Millisecond) // Запускаем сервис service.Start() @@ -49,7 +50,7 @@ func TestService_StopWithDoneChannel(t *testing.T) { time.Sleep(25 * time.Millisecond) // Останавливаем сервис - close(done) + cancel() // Даем время на обработку завершения time.Sleep(30 * time.Millisecond) @@ -71,11 +72,11 @@ func TestService_LimitTenNotes(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(log.Writer()) - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - service := NewService(repo, done, 5*time.Millisecond) + service := NewService(repo, ctx, 5*time.Millisecond) // Запускаем сервис service.Start() @@ -98,11 +99,11 @@ func TestService_LimitTenNotes(t *testing.T) { // TestService_NoteTitles тестирует корректность заголовков заметок func TestService_NoteTitles(t *testing.T) { - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - service := NewService(repo, done, 10*time.Millisecond) + service := NewService(repo, ctx, 10*time.Millisecond) // Запускаем сервис service.Start() @@ -128,13 +129,13 @@ func TestService_NoteTitles(t *testing.T) { // TestService_ConcurrentSafety тестирует безопасность конкурентного доступа func TestService_ConcurrentSafety(t *testing.T) { - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() // Создаем сервис - service := NewService(repo, done, 15*time.Millisecond) + service := NewService(repo, ctx, 15*time.Millisecond) // Запускаем сервис service.Start() @@ -153,10 +154,10 @@ func TestService_ChannelBlocking(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(log.Writer()) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) repo := repository.NewRepository() - service := NewService(repo, done, 5*time.Millisecond) + service := NewService(repo, ctx, 5*time.Millisecond) // Запускаем сервис service.Start() @@ -165,12 +166,12 @@ func TestService_ChannelBlocking(t *testing.T) { time.Sleep(30 * time.Millisecond) // Останавливаем сервис - close(done) + cancel() time.Sleep(20 * time.Millisecond) logOutput := buf.String() - // Сервис должен корректно завершиться по сигналу done + // Сервис должен корректно завершиться по сигналу контекста assert.Contains(t, logOutput, "Сервис: завершение", "Сервис должен корректно завершиться. Вывод: %s", logOutput) } @@ -181,13 +182,13 @@ func TestService_ImmediateStop(t *testing.T) { log.SetOutput(&buf) defer log.SetOutput(log.Writer()) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) // Останавливаем сервис сразу же - close(done) + cancel() repo := repository.NewRepository() - service := NewService(repo, done, 10*time.Millisecond) + service := NewService(repo, ctx, 10*time.Millisecond) service.Start() // Даем время на обработку @@ -206,11 +207,11 @@ func TestService_ImmediateStop(t *testing.T) { // TestService_NoteContent тестирует содержимое заметок func TestService_NoteContent(t *testing.T) { - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - service := NewService(repo, done, 10*time.Millisecond) + service := NewService(repo, ctx, 10*time.Millisecond) // Запускаем сервис service.Start() @@ -231,11 +232,11 @@ func TestService_NoteContent(t *testing.T) { // TestService_SimpleCase тестирует простой сценарий работы сервиса func TestService_SimpleCase(t *testing.T) { - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() - service := NewService(repo, done, 30*time.Millisecond) + service := NewService(repo, ctx, 30*time.Millisecond) // Запускаем сервис service.Start() @@ -249,14 +250,14 @@ func TestService_SimpleCase(t *testing.T) { // TestService_MultipleInstances тестирует работу нескольких экземпляров сервиса func TestService_MultipleInstances(t *testing.T) { - done := make(chan struct{}) - defer close(done) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() repo := repository.NewRepository() // Создаем два сервиса - service1 := NewService(repo, done, 20*time.Millisecond) - service2 := NewService(repo, done, 25*time.Millisecond) + service1 := NewService(repo, ctx, 20*time.Millisecond) + service2 := NewService(repo, ctx, 25*time.Millisecond) // Запускаем оба сервиса service1.Start() From af2c0ecf93b8c8879ba40fa54eda600df2f38a5d Mon Sep 17 00:00:00 2001 From: RD2W Date: Thu, 6 Nov 2025 18:36:16 +0300 Subject: [PATCH 2/2] refactor: extract constants and remove unused ones --- cmd/notes/main.go | 7 ++----- go.sum | 1 + internal/service/service.go | 9 +++++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/notes/main.go b/cmd/notes/main.go index 5112876..5b33e33 100644 --- a/cmd/notes/main.go +++ b/cmd/notes/main.go @@ -19,19 +19,16 @@ import ( const ( LoggerInterval = 200 * time.Millisecond DataGenInterval = 500 * time.Millisecond - AppRunDuration = 6 * time.Second GracefulShutdownDelay = 100 * time.Millisecond - EntityChanBuffer = 10 - TimeFormat = "2006-01-02 15:04:05" ) // Строковые константы const ( AppStartMsg = "Запуск приложения с горутинами и каналами..." - DataGenStartMsg = "Запуск генерации тестовых данных..." AppShutdownMsg = "Приложение \"Заметки\" успешно завершило выполнение программы!" + ShutdownStartMsg = "Получен сигнал завершения, инициируем graceful shutdown..." ResultsHeader = "\n=== РЕЗУЛЬТАТЫ ===\n" NoteCountMsg = "Всего заметок создано: %d\n" NoteDoesNotExistMsg = "Ошибка: заметка не существует" @@ -64,7 +61,7 @@ func main() { // Ждем сигнал завершения <-sigChan - log.Println("Получен сигнал завершения, инициируем graceful shutdown...") + log.Println(ShutdownStartMsg) // Отменяем контекст для завершения всех горутин cancel() diff --git a/go.sum b/go.sum index c4c1710..aa493cc 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/service/service.go b/internal/service/service.go index 86d8b04..6dcd984 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -10,6 +10,11 @@ import ( "github.com/rd2w/go-notes/internal/repository" ) +const ( + EntityChanBuffer = 10 + MaxNotesCount = 10 +) + // Service содержит бизнес-логику приложения type Service struct { repo *repository.Repository @@ -29,7 +34,7 @@ func NewService(repo *repository.Repository, ctx context.Context, interval time. // Start запускает все горутины сервиса func (s *Service) Start() { // Создаем канал для передачи сущностей между горутинами - entityChan := make(chan repository.Entity, 10) + entityChan := make(chan repository.Entity, EntityChanBuffer) // Запускаем горутину для генерации данных go s.startDataGeneration(entityChan) @@ -62,7 +67,7 @@ func (s *Service) startDataGeneration(entityChan chan<- repository.Entity) { } noteCounter++ - if noteCounter > 10 { + if noteCounter > MaxNotesCount { log.Println("Сервис: генерация тестовых данных завершена") return }