Есть простой файловый логгер, его код трогать нельзя. Сначала назови его
недостатки. Затем напиши обёртку QueuedLogger: она складывает сообщения в
очередь, а в файл их пишет отдельная горутина. Тогда медленная запись (большое
сообщение, тормозящий диск) не держит вызывающий код.
type Logger interface {
Log(msg string) error
Close() error
}
type FileLogger struct {
f *os.File
}
func NewFileLogger(name string) (*FileLogger, error) {
f, e := os.Create(name)
if e != nil {
return nil, e
}
return &FileLogger{f}, nil
}
func (l *FileLogger) Log(msg string) error {
_, e := l.f.WriteString(msg + "\n")
return e
}
func (l *FileLogger) Close() error {
return l.f.Close()
}
// ======= КОД ВЫШЕ НЕ ТРОГАЕМ =========
Что нужно написать:
var (
ErrQueueFull = errors.New("log queue is full")
ErrClosed = errors.New("logger is closed")
)
// NewQueuedLogger оборачивает inner очередью на size сообщений (size < 1 считается как 1).
func NewQueuedLogger(inner Logger, size int) *QueuedLogger
func (q *QueuedLogger) Log(msg string) error
func (q *QueuedLogger) Close() error
Требования
Log никогда не блокируется на записи. Сообщение встаёт в очередь, и
возвращается nil. Если очередь заполнена, сразу возвращается ErrQueueFull,
а сообщение отбрасывается.
inner.Log вызывается из одной горутины, то есть никогда не параллельно.
Сообщения пишутся в том порядке, в котором их приняли.
Close перестаёт принимать сообщения, дописывает всё, что уже приняли
(каждый Log, вернувший nil), и только потом вызывает inner.Close().
Возвращается после этого.
Close возвращает errors.Join(первая ошибка inner.Log, ошибка inner.Close()):
если какая-то запись упала, вызывающий об этом узнает.
Log после Close возвращает ErrClosed без паники, даже если вызовы идут
конкурентно с Close. Повторный Close возвращает nil, а inner.Close()
вызывается ровно один раз.
- После
Close горутин не остаётся. Код проходит go test -race.
Пример
fl, _ := NewFileLogger("app.log")
q := NewQueuedLogger(fl, 128)
q.Log("started") // nil, сразу
q.Log(hugeDump) // nil, сразу: запись идёт в фоне
_ = q.Close() // ждёт, пока оба сообщения окажутся в файле
q.Log("late") // ErrClosed
Теряющая сообщения очередь подходит не всем. Сделай второй вариант, у которого
очередь не ограничена и построена на container/list.
func NewListLogger(inner Logger) *ListLogger
func (l *ListLogger) Log(msg string) error
func (l *ListLogger) Close() error
Log не блокируется на записи и не теряет сообщения. ErrQueueFull не
бывает.
- Писатель не крутится в пустом цикле, пока очередь пуста, а спит до прихода
сообщения.
- Остальное, как у
QueuedLogger: один писатель, порядок, Close дописывает
хвост, ErrClosed, ошибки через errors.Join, нет утечек.
Устно: какие недостатки у FileLogger? Какую проблему решает очередь и
какую создаёт: что теряется при падении процесса, что делать при переполнении
(блокировать, отбрасывать новые, вытеснять старые)?