Skip to content

Commit

Permalink
Merge pull request #30 from thefrol/iter13
Browse files Browse the repository at this point in the history
Итерация 13. Интроспекция ошибок
  • Loading branch information
thefrol authored Oct 21, 2023
2 parents 94d4ca0 + 71e09be commit 3d7f5f7
Show file tree
Hide file tree
Showing 15 changed files with 416 additions and 44 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@

## Временные комменты

Когда добавляешь новый код, или поле в структуру, можно написать над этим полем комментарий. Потом его можно будет стереть, но мне кажется прикольно, что в коммите навсегда останется намерение с котором было сделано изменение.
Когда добавляешь новый код, или поле в структуру, можно написать над этим полем комментарий. Потом его можно будет стереть, но мне кажется прикольно, что в коммите навсегда останется намерение с котором было сделано изменение.

## А ещё

Expand Down
2 changes: 1 addition & 1 deletion cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func ConfigureStorage(cfg config) (api.Operator, context.CancelFunc) {
panic(e)
}

dbs, err := storage.NewDatabase(db) // todo зачем тут контекст???
dbs, err := storage.NewDatabase(db)
if err != nil {
log.Error().Msgf("Ошибка создания хранилища в базе данных - %v", err)
panic(err)
Expand Down
20 changes: 19 additions & 1 deletion internal/report/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/thefrol/kysh-kysh-meow/internal/metrica"
"github.com/thefrol/kysh-kysh-meow/lib/retry"
"github.com/thefrol/kysh-kysh-meow/lib/retry/fails"
)

var (
Expand All @@ -34,7 +36,23 @@ func Send(metricas []metrica.Metrica, url string) error {
// у нас существует очень важный контракт,
// что тело сюда передается в формате io.Reader,
// тогда могут работать разные мидлвари
resp, err := defaultClient.R().SetBody(buf).Post(url) // todo в данный момент мы не используем тут easyjson
var resp *resty.Response
sendCall := func() error {
var err error
resp, err = defaultClient.R().SetBody(buf).Post(url)
return err
}

// запустим отправку с тремя попытками дополнительными
err = retry.This(sendCall,
retry.Attempts(3),
retry.DelaySeconds(1, 3, 5, 7),
retry.If(fails.OnDial), // черт, так красиво
retry.OnRetry(func(n int, err error) {
log.Info().Msgf("%v попытка отправить данные, ошибка: %v", n, err)
}),
)
// todo в данный момент мы не используем тут easyjson

if err != nil {
log.Error().Str("location", "internal/report").Msgf("Не могу отправить сообщение c пачкой метрик по приничине %+v", err)
Expand Down
3 changes: 2 additions & 1 deletion internal/server/api/batch_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package api
import (
"context"
"encoding/json"
"errors"
"net/http"

"github.com/thefrol/kysh-kysh-meow/internal/metrica"
Expand Down Expand Up @@ -48,7 +49,7 @@ func HandleJSONBatch(handler func(context.Context, ...metrica.Metrica) (out []me
// Запускаем обработчик handler()
out, err := handler(r.Context(), in...)
if err != nil {
if err == ErrorNotFoundMetric {
if errors.Is(err, ErrorNotFoundMetric) {
HTTPErrorWithLogging(w, http.StatusNotFound, "В хранилище не найдена одна из метрик %v", in)
return
}
Expand Down
73 changes: 73 additions & 0 deletions internal/server/api/error.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package api

import (
"context"
"fmt"
"net/http"

"github.com/rs/zerolog/log"
"github.com/thefrol/kysh-kysh-meow/lib/retry"
"github.com/thefrol/kysh-kysh-meow/lib/retry/fails"
)

// httpErrorWithLogging отправляет сообщение об ошибке, параллельно дублируя в журнал. Работает быстрее, чем просто две функции отдельно.
Expand All @@ -21,3 +24,73 @@ func HTTPErrorWithLogging(w http.ResponseWriter, statusCode int, format string,
//
// Возможно это пока единственный повод держать кастомный логгер, чтобы в нем была функция типа withHttpError(w)
}

// Retry3Times повторяет операцию op ровно три раза
// с промежутками 1,3,5 секунд.
//
// retriableHandler:=Retry3Times(handler)
// out, err:=retryableHandler(ctx, in)
func Retry3Times(op Operation) Operation {
return func(ctx context.Context, d ...datastruct) (out []datastruct, err error) {
err =
retry.This(
func() error {
out, err = op(ctx, d...)
return err
},
retry.If(fails.OnDial),
retry.Attempts(3),
retry.DelaySeconds(1, 3, 5, 7),
retry.OnRetry(
func(i int, err error) {
log.Info().Msgf("Выполняется повторная попытка %v после ошибки %v", i, err)
}),
)
if err != nil {
return nil, err
}
return
}
}

// RetryWithTimeouts повторяет операцию c паузузами между операциями,
// указанными при закуске. Количество повторений зависит от колчества таймаутов
// Если таймауты не указаны, то используется три повтора с интевалами 1,3,5 секунд
//
// retriableHandler:=Retry3Times(handler)
// out, err:=retryableHandler(ctx, in)
func RetryWithTimeouts(op Operation, timeOutSeconds ...uint) Operation {
return func(ctx context.Context, d ...datastruct) (out []datastruct, err error) {
var (
timeouts retry.Option
attempts retry.Option
)

if len(timeOutSeconds) == 0 {
timeouts = retry.DelaySeconds(1, 3, 5)
attempts = retry.Attempts(3)
} else {
timeouts = retry.DelaySeconds(timeOutSeconds...)
attempts = retry.Attempts(uint(len(timeOutSeconds)))
}

err =
retry.This(
func() error {
out, err = op(ctx, d...)
return err
},
retry.If(fails.OnDial),
attempts,
timeouts,
retry.OnRetry(
func(i int, err error) {
log.Info().Msgf("Выполняется повторная попытка %v после ошибки %v", i, err)
}),
)
if err != nil {
return nil, err
}
return
}
}
3 changes: 2 additions & 1 deletion internal/server/api/json_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package api

import (
"context"
"errors"
"net/http"

"github.com/mailru/easyjson"
Expand Down Expand Up @@ -59,7 +60,7 @@ func HandleJSONRequest(handler func(context.Context, ...metrica.Metrica) (out []
// Запускаем обработчик handler()
arr, err := handler(r.Context(), in)
if err != nil {
if err == ErrorNotFoundMetric {
if errors.Is(err, ErrorNotFoundMetric) {
HTTPErrorWithLogging(w, http.StatusNotFound, "В хранилище не найдена метрика %v", in.ID)
return
}
Expand Down
7 changes: 4 additions & 3 deletions internal/server/api/url_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package api

import (
"errors"
"net/http"
"strconv"

Expand Down Expand Up @@ -31,12 +32,12 @@ func HandleURLRequest(op Operation) http.HandlerFunc {
arr, err := op(r.Context(), in)

if err != nil { // todo вот этот код встречается в соседних обертках
if err == ErrorDeltaEmpty || err == ErrorValueEmpty {
if errors.Is(err, ErrorDeltaEmpty) || errors.Is(err, ErrorValueEmpty) {
HTTPErrorWithLogging(w, http.StatusBadRequest, "Ошибка входных данных: %v", err)
} else if err == ErrorNotFoundMetric {
} else if errors.Is(err, ErrorNotFoundMetric) {
HTTPErrorWithLogging(w, http.StatusNotFound, "Не найдена метрика %v с именем %v", in.MType, in.ID)
return
} else if err == ErrorUnknownMetricType {
} else if errors.Is(err, ErrorUnknownMetricType) {
HTTPErrorWithLogging(w, http.StatusBadRequest, "Неизвестный тип счетчика: %v", in.MType)
return
}
Expand Down
32 changes: 30 additions & 2 deletions internal/server/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,34 @@ router.Post("/ping", App.Ping)
router.Get("/list", App.List)
```

`Ping()` переедет из `store` в `App`, и хоть это может показаться глуповатым на первый взгляд, стоит отментить задание. А в нашей архитектуре мы стараемся держаться поближе к предметами реального мира. `/ping` должно работать именно с БД. А стораж не знает в общем случае, БД он или нет.
`Ping()` переедет из `store` в `App`, и хоть это может показаться глуповатым на первый взгляд, стоит отментить задание. А в нашей архитектуре мы стараемся держаться поближе к предметами реального мира. `/ping` должно работать именно с БД. А стораж не знает в общем случае, БД он или нет.

`Operator` скорее всего передет из хендлеров. И это позволит избавиться от зависимости к хранилищу у хелдлеров. Связывать `handlers` и `storage` мы будем через `app`. Таким образом и хендлеры и хранилище получяит зависимостиь от более высокоуровнего СЕРВИСА, но при этом освободятся от зависимостей между друг другом. При этом App тоже не будет от них зависеть. Отличная тема однако!
`Operator` скорее всего передет из хендлеров. И это позволит избавиться от зависимости к хранилищу у хелдлеров. Связывать `handlers` и `storage` мы будем через `app`. Таким образом и хендлеры и хранилище получяит зависимостиь от более высокоуровнего СЕРВИСА, но при этом освободятся от зависимостей между друг другом. При этом App тоже не будет от них зависеть. Отличная тема однако.

### Ошибки

Тоже перетекают в апп

### Operator

Отправится в `app`, а вот `Operator` пока останется в `api`

## А что дельше?

Мне кажется, что мне не нужны фабричные методы, вместо этого мне нужно компоновать методы во что-то большее, типа

```go
router.Post("/list",Handler(app.ListMetrics))

gRPC.AddService("/list",grpcHandler(app.ListMetrics))

func (api App) List(attrs ...atts) []rets{
// validate
validate(in)

//handle
out:=hanlde(app.store)
}
```

Типа эти функции компонуются из других функций. И потом эти бизнес правила могут использоваться как в `HTTP`, так и в `gRPC`
16 changes: 8 additions & 8 deletions internal/server/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,20 @@ func MeowRouter(store api.Operator) (router chi.Router) {
router.Group(func(r chi.Router) {
// в какой-то момент, когда починят тесты, тут можно будет снять комменты
//r.With(chimiddleware.AllowContentType("text/plain")) todo
r.Get("/value/{type}/{name}", api.HandleURLRequest(store.Get))
r.Post("/update/{type}/{name}/{value}", api.HandleURLRequest(store.Update))
r.Get("/value/{type}/{name}", api.HandleURLRequest(api.Retry3Times(store.Get)))
r.Post("/update/{type}/{name}/{value}", api.HandleURLRequest(api.Retry3Times(store.Update)))
})

// Создаем маршруты для обработки JSON запросов
router.Group(func(r chi.Router) {
r.With(chimiddleware.AllowContentType("application/json"))

r.Post("/value", api.HandleJSONRequest(store.Get))
r.Post("/value/", api.HandleJSONRequest(store.Get))
r.Post("/update", api.HandleJSONRequest(store.Update))
r.Post("/update/", api.HandleJSONRequest(store.Update))
r.Post("/updates", api.HandleJSONBatch(store.Update))
r.Post("/updates/", api.HandleJSONBatch(store.Update))
r.Post("/value", api.HandleJSONRequest(api.Retry3Times(store.Get)))
r.Post("/value/", api.HandleJSONRequest(api.Retry3Times(store.Get)))
r.Post("/update", api.HandleJSONRequest(api.Retry3Times(store.Update)))
r.Post("/update/", api.HandleJSONRequest(api.Retry3Times(store.Update)))
r.Post("/updates", api.HandleJSONBatch(api.Retry3Times(store.Update)))
r.Post("/updates/", api.HandleJSONBatch(api.Retry3Times(store.Update)))

// TODO
//
Expand Down
46 changes: 20 additions & 26 deletions internal/storage/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/thefrol/kysh-kysh-meow/internal/metrica"
"github.com/thefrol/kysh-kysh-meow/internal/server/api"
"github.com/thefrol/kysh-kysh-meow/lib/retry"
"github.com/thefrol/kysh-kysh-meow/lib/retry/fails"
)

var (
Expand All @@ -23,8 +25,7 @@ const (

queryList = "SELECT 'counter',id FROM counters UNION SELECT 'gauge',id from gauges;"

queryUpdateCounter = "UPDATE counters SET delta=delta+$2 WHERE id=$1"
queryInsertCounter = "INSERT INTO counters VALUES ($1,$2);"
queryUpsertCounter = "INSERT INTO counters VALUES ($1,$2) ON CONFLICT (id) DO UPDATE SET delta=counters.delta+$2;"
queryUpsertGauge = "INSERT INTO gauges VALUES ($1,$2) ON CONFLICT (id) DO UPDATE SET value=$2;"
)

Expand All @@ -38,7 +39,20 @@ func NewDatabase(db *sql.DB) (*Database, error) {
// инициализуем таблицы для гаужей и каунтеров
//
// todo использовать транзации с отменой
_, err := db.Exec(initQuery)
err :=
retry.This(
func() error {
_, err := db.Exec(initQuery)
return err
},
retry.If(fails.OnDial),
retry.Attempts(3),
retry.DelaySeconds(1, 3, 5, 7),
retry.OnRetry(
func(i int, err error) {
log.Info().Msgf("Попытка инициализации базы %v: %v", i, err)
}))

if err != nil {
return nil, ErrorInitDatabase
}
Expand Down Expand Up @@ -69,7 +83,7 @@ func (d *Database) Get(ctx context.Context, req ...metrica.Metrica) (resp []metr
err := rw.Scan(&result.ID, result.Delta)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, api.ErrorNotFoundMetric
return nil, api.ErrorNotFoundMetric // todo, вот тут можно упаковку ошибки сделать впринципе
}
return nil, err
}
Expand All @@ -81,7 +95,7 @@ func (d *Database) Get(ctx context.Context, req ...metrica.Metrica) (resp []metr
err := rw.Scan(&result.ID, result.Value)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, api.ErrorNotFoundMetric
return nil, api.ErrorNotFoundMetric // todo упаковать ошибку???
}
return nil, err
}
Expand Down Expand Up @@ -132,34 +146,14 @@ func (d *Database) Update(ctx context.Context, req ...metrica.Metrica) (resp []m
for _, r := range req {
switch r.MType {
case "counter":
/*
1. Пробуем делать UPDATE ... SET value=value+delta
2. Если количество обновленных полей =0, делаем INSERT INTO
*/

if r.Delta == nil {
return nil, api.ErrorDeltaEmpty
}

// todo не помню, надо ли тут проверять на всякие пустые ссылки...
rs, err := tx.ExecContext(ctx, queryUpdateCounter, r.ID, r.Delta)
if err != nil {
return nil, err
}
count, err := rs.RowsAffected()
_, err := tx.ExecContext(ctx, queryUpsertCounter, r.ID, r.Delta)
if err != nil {
return nil, err
}
if count > 1 {
log.Warn().Msgf("При транзакции обновлено несколько строк %+v", r)
}
if count == 0 {
// Значит счетчик не создан, значит создадим
_, err := tx.ExecContext(ctx, queryInsertCounter, r.ID, r.Delta)
if err != nil {
return nil, err
}
}

case "gauge":
if r.Value == nil {
Expand Down
Loading

0 comments on commit 3d7f5f7

Please sign in to comment.