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
33 changes: 20 additions & 13 deletions cmd/notes/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/rd2w/go-notes/internal/logger"
Expand All @@ -15,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 = "Ошибка: заметка не существует"
Expand All @@ -41,23 +42,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(ShutdownStartMsg)

// Сигнал завершения всем горутинам
close(done)
// Отменяем контекст для завершения всех горутин
cancel()

// Даем время на корректное завершение
time.Sleep(GracefulShutdownDelay)
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
9 changes: 5 additions & 4 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger

import (
"context"
"log"
"time"

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -49,7 +50,7 @@ func (l *Logger) Start() {

lastNoteCount = currentNoteCount
}
case <-l.done:
case <-l.ctx.Done():
log.Println("Логгер: завершение работы")
return
}
Expand Down
69 changes: 35 additions & 34 deletions internal/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package logger

import (
"bytes"
"context"
"log"
"strings"
"sync"
Expand Down Expand Up @@ -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()
Expand All @@ -52,8 +53,8 @@ func TestLogger_IntegrationWithService(t *testing.T) {
// Ждем достаточно времени для обработки нескольких итераций
time.Sleep(100 * time.Millisecond)

// Закрываем канал после проверки
close(done)
// Завершаем работу через контекст
cancel()

// Ждем немного, чтобы логгер успел завершить работу и вывести сообщения
time.Sleep(10 * time.Millisecond)
Expand All @@ -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()
Expand All @@ -103,7 +104,7 @@ func TestLogger_StopWithDoneChannel(t *testing.T) {
time.Sleep(20 * time.Millisecond)

// Теперь останавливаем ДО того как логгер успеет проверить
close(done)
cancel()

// Даем время на завершение
time.Sleep(50 * time.Millisecond)
Expand All @@ -115,7 +116,7 @@ func TestLogger_StopWithDoneChannel(t *testing.T) {

// В этом тесте мы специально останавливаем логгер ДО того как он проверит заметки
// Поэтому он может не успеть залогировать заметки - это нормальное поведение
t.Logf("Тест завершен: логгер корректно остановился по сигналу done")
t.Logf("Тест завершен: логгер корректно остановился по сигналу контекста")
}

func TestLogger_MultipleNoteGeneration(t *testing.T) {
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -362,7 +363,7 @@ func TestLogger_SeesNotesBeforeStop(t *testing.T) {
time.Sleep(30 * time.Millisecond)

// Теперь останавливаем
close(done)
cancel()

// Даем время на завершение
time.Sleep(20 * time.Millisecond)
Expand All @@ -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()
Expand All @@ -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()
Expand Down
22 changes: 14 additions & 8 deletions internal/service/service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"context"
"fmt"
"log"
"time"
Expand All @@ -9,26 +10,31 @@ import (
"github.com/rd2w/go-notes/internal/repository"
)

const (
EntityChanBuffer = 10
MaxNotesCount = 10
)

// 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,
}
}

// Start запускает все горутины сервиса
func (s *Service) Start() {
// Создаем канал для передачи сущностей между горутинами
entityChan := make(chan repository.Entity, 10)
entityChan := make(chan repository.Entity, EntityChanBuffer)

// Запускаем горутину для генерации данных
go s.startDataGeneration(entityChan)
Expand All @@ -55,17 +61,17 @@ 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
}

noteCounter++
if noteCounter > 10 {
if noteCounter > MaxNotesCount {
log.Println("Сервис: генерация тестовых данных завершена")
return
}
case <-s.done:
case <-s.ctx.Done():
log.Println("Сервис: завершение работы генерации по сигналу")
return
}
Expand All @@ -81,7 +87,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
}
Expand Down
Loading