Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/sheeiavellie/avito040424

Test task for Golang intern position at Avito 04/04/24
https://github.com/sheeiavellie/avito040424

Last synced: 4 days ago
JSON representation

Test task for Golang intern position at Avito 04/04/24

Awesome Lists containing this project

README

        

# Banner Service
*если вы читаете это до 23:59 14-го апреля, то тут еще может появится что-то новое*
### Как поставить
1. Удостовериться, что есть git, docker, make и go;
2. Клонируем репу;
3. Добавляем .env со всем необходимым в корень проекта:
```
PORT=1337
HOST=localhost
PS_USER=amogus
PS_PASSWORD=amogus
PS_PORT=5432
PS_DB_NAME=banner_service_db
REDIS_PASSWORD=amogus
```
4. Поднимает контейнеры:
```sh
docker-compose up
```
5. Билдим и запускаем:
```sh
make run
```

### Стэк
Go (стандартная библиотека + пара удобных штук + [golang-lru](https://github.com/hashicorp/golang-lru));
PostgreSQL.

### Проблемы и их решение
- Общий подход и идея:

Так как по условию сервис не имеет большого количество тегов/фичей/баннеров, то я решил не использовать
крупные фреймворки, а сделать все минималистично. В конце-концов, пусть в тексте задания об этом и не
говорилось, но очевидно, что это микросервис. Поэтому я решил использовать стандартную библиотеку без
каких либо больших абстракций внутри самого проекта. В итоге, самое абстрактное, что у меня есть -
это ```BannerRepository```. Однако, можно было бы обойтись и без него, если бы не необходимость в
кэшировании. Без общей абстрактной штуки было бы слишком много ненужного кода в handler'ах, а с
ним получилось просто вызывать один метод и бесконечно обрабатывать ошибки.

- Кэширование:

Решил использовать LRU кэш. При этом, решил не использовать Redis, так как
в данном сервисе это ту мач, плюс Redis уже не open-source. Думаю, что in-memory
LRU кэша будет вполне достаточно. Тем не менее, если необходимо расти в ширину,
то сюда можно всегда поставить Redis без особых трудностей.

- Вычисляем ключ:

Другой интересной задачкой была задачка о том, как сделать ключ. Решил пойти по
тупому - просто отсортировал id фичи и id тегов. Получился ключ. Конечно, если
делать по умному, то хорошо было бы использовать хэш функцию. Хотя возможно это не
самое лучшее решение просто из-за коллизий.

- Валидация токенов и вообще про токены:

Ну, в задании об этом сказано не было, но, как я полагаю, токен должен иметь какой-нибудь формат.
Я решил просто закинуть payload вида ```{"role":"admin"}``` в base64 и кидать в запросе в
header'е token. Касательно валидации - я просто вынес это все в отдельную мидлвару. Возможно,
если сервису потребуется использовать *реальный* токен, то решение с миддлварой может быть пересмотрено.

- База данных и массивы:

Учитывая, что у множества баннеров может быть множество тегов, стоило завести дополнительную табличку,
к примеру, ```banners_to_tags``` ну или что-то подобное. Я же решил не идти по этому пути ввиду
скуки и того, что подобные вещи я уже имплементировал. Однако, я никогда не работал с массивами в
Postgres, собственно поэтому я решил не создавать нормальное отношение, а использовать INTEGER[].

- Patch запрос:

Отдельной болью в голове был Patch запрос. Мое решение, как мне кажется, не самое лаконичное и *правильное*,
но я решил делать так, просто потому, что мне захотелось попрактиковаться с каналами. Вышло не так уж и плохо,
но я понимаю, что можно сделать еще лучше. Главной проблемой была возможность передать лишь часть полей, однако
очевидно, что остальные, не переданные поля, меняться были бы не должны. Изначально, я думал о том, чтобы написать
что-то вроде контекста для ```BannerPatchRequest```, в нем была бы мапа или же массив, длина которого равна кол-ву полей
структуры ```BannerPatchRequest```. Далее, в массиве бы лежали строки, которые потом могли бы подставляться в запрос
(строки типа "feature_id = "). Ну и самое интересно, можно было бы написать имплементацию метода ```UnmarshalJSON(d []byte) error```
для структуры ```BannerPatchRequest```. Там бы можно было бы понять, каким полям мы бы присваивали новые значения,
соответственно можно было бы сохранять состояния для строк в массиве. В итоге, передав структуру в метод, делающий запрос к базе,
можно было бы одной строкой достать все поля, которые должны были бы быть изменены. Но потом я осмыслил себя и поняд, что такое
решение это прямо таки too much. И решил просто получать баннер из базы, используя это как дефолтные значения для структуры с запросом,
куда бы потом уже бы анмаршелился сам JSON запрос.

- Контексты:

Не делал полноценную отмену запроса для админских запросов, но сделал для пользовательского (чуть-чуть не успел).
Вероятно, было бы хорошей идеей вынести это все в отдельную миддлвару. Для админских запросов есть просто контекс
с таймаутом, который передается и в базу в том числе. Подумал упростить себе жизнь, ведь так или иначе админский запрос
отменится, если истичет контекст, ведь база выдаст ошибку. С пользовательским запросом уже так не получилось бы, так как
там нужно кэшировать.

- Об общих проблемах:
Самой большой проблемой для меня был нейминг всего и вся. Я как-то даже через чур много времени потратил, думая о том,
как было бы лучше назвать очередную новую штуку. Или о том, что мне надо пересмотреть названия вообще всех стркутур,
так как что-то становится семантически глупым. Вероятно, лучшее это враг хорошего.

### Занимательные штуки, которое я написал:
Тут опишу небольшие классные штуки, которые удалось сделать в процессе выполнения тестового:
- Написал занятную обертку для транзакций:
```golang
func (ps *PostgresStorage) execTx(
ctx context.Context,
opts *sql.TxOptions,
f func(tx *sql.Tx) error,
) error {
tx, err := ps.db.BeginTx(ctx, opts)
if err != nil {
return fmt.Errorf("error executing tx: %w", err)
}
defer func() {
_ = tx.Rollback()
}()

execErr := f(tx)
if execErr != nil {
_ = tx.Rollback()
return fmt.Errorf("error executing tx: %w", execErr)
}

if err := tx.Commit(); err != nil {
return fmt.Errorf("error executing tx: %w", err)
}
return nil
}
```
Она получает на вход функцию, в которой уже выполняется один из дефотных
методов из пакета sql. При этом, если подразумевается возвращение результата,
то можно воспользоваться замыканием, объявив переменную для записи результата
до вызова ```execTX(...)```.

- Добавил крутые неизменяемые роли:
```golang
package api

var (
AdminRole = &accessRole{name: "admin"}
UserRole = &accessRole{name: "user"}
)

type AccessRole interface {
GetName() string
}

type accessRole struct {
name string
}

func (ar *accessRole) GetName() string {
return ar.name
}
```
- Ну и handler для Patch запроса (не буду вставлять его код сюда).

### Линтинг
Не использую линтеры как таковые, хотя очевидно осведомлен об их существовании. У меня в NeoVim стоит лишь автоформаттинг при
сохранении файла, а также я переношу строки длинее 80 символов.