From 49c962e13c0fee7f3681a23f03e2e23e35f3cab6 Mon Sep 17 00:00:00 2001 From: derfenix Date: Thu, 24 Aug 2023 23:40:31 +0300 Subject: [PATCH] Initial commit --- .gitignore | 43 ++++ .idea/.gitignore | 8 + .idea/git_toolbox_prj.xml | 15 ++ .idea/golinter.xml | 17 ++ .idea/modules.xml | 8 + .idea/protect_trans_info.iml | 9 + .idea/vcs.xml | 6 + .idea/yamllint.xml | 7 + Makefile | 9 + README.md | 46 ++++ adapters/inmemorycache/cache.go | 11 + application/application.go | 69 ++++++ application/config.go | 27 +++ application/migration.go | 37 ++++ .../20230824180522_conn_logs_table.up.sql | 15 ++ application/repository/bunpg.go | 23 ++ application/repository/logs.go | 142 +++++++++++++ application/repository/logs_test.go | 151 +++++++++++++ bench_test.go | 30 +++ cmd/migrate/main.go | 36 ++++ cmd/seed/main.go | 41 ++++ cmd/service/main.go | 56 +++++ deploy/Dockerfile | 18 ++ docker-compose.yaml | 49 +++++ go.mod | 54 +++++ go.sum | 198 ++++++++++++++++++ port/http/service.go | 144 +++++++++++++ scripts/seed.go | 75 +++++++ test.http | 7 + testdata/fuzz/FuzzDup/149d6ad96b2f24d6 | 3 + testdata/fuzz/FuzzDup/29aa5edb5a37da5e | 3 + testdata/fuzz/FuzzDup/642e3858300a23d4 | 3 + 32 files changed, 1360 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/git_toolbox_prj.xml create mode 100644 .idea/golinter.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/protect_trans_info.iml create mode 100644 .idea/vcs.xml create mode 100644 .idea/yamllint.xml create mode 100644 Makefile create mode 100644 README.md create mode 100644 adapters/inmemorycache/cache.go create mode 100644 application/application.go create mode 100644 application/config.go create mode 100644 application/migration.go create mode 100644 application/migrations/20230824180522_conn_logs_table.up.sql create mode 100644 application/repository/bunpg.go create mode 100644 application/repository/logs.go create mode 100644 application/repository/logs_test.go create mode 100644 bench_test.go create mode 100644 cmd/migrate/main.go create mode 100644 cmd/seed/main.go create mode 100644 cmd/service/main.go create mode 100644 deploy/Dockerfile create mode 100644 docker-compose.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 port/http/service.go create mode 100644 scripts/seed.go create mode 100644 test.http create mode 100644 testdata/fuzz/FuzzDup/149d6ad96b2f24d6 create mode 100644 testdata/fuzz/FuzzDup/29aa5edb5a37da5e create mode 100644 testdata/fuzz/FuzzDup/642e3858300a23d4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04d0201 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +.idea/sonarlint/ +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/golinter.xml b/.idea/golinter.xml new file mode 100644 index 0000000..112f673 --- /dev/null +++ b/.idea/golinter.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b8ee50a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/protect_trans_info.iml b/.idea/protect_trans_info.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/protect_trans_info.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/yamllint.xml b/.idea/yamllint.xml new file mode 100644 index 0000000..e3ff02e --- /dev/null +++ b/.idea/yamllint.xml @@ -0,0 +1,7 @@ + + + + true + /usr/bin/yamllint + + \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f8675b9 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +first_run: + docker compose up -d db + @sleep 2 + docker compose up -d migrate + docker compose up seed + docker compose up service + +start: + docker compose up service diff --git a/README.md b/README.md new file mode 100644 index 0000000..10901a5 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +## Запуск + +### Первый запуск: +```shell +make first_run +``` + + Команда запустит БД (PostgreSQL 15), Rest-сервис, + накатит миграцию в БД и заполнит базу рандомными данными + (2 миллиона записей). + +### Повторный запуск сервиса: +```shell +make start +``` + +### Вызов метода проверки дубликатов: +```shell +curl -X GET --location "http://127.0.0.1:8001/124/432" +``` + +### Отдельно добавил выгрузку тестовых данных из БД: +```shell +curl -X GET --location "http://127.0.0.1:8001/list" > data.txt +``` + +### Запрос с гарантировано дублируемыми IP: +```shell +curl -X GET --location "http://127.0.0.1:8001/88888/99999" +``` + + +## Комментарии к реализации + +Есть тесты на основной методв репозитория (с использованием docker через +либу dockertest), на остальное тестов не делал. Добавил ещё бэнчмарк и fuzzy-тест, +но они работать будут только при запущенном в докере сервисе. + +При запуске вся база подтягивается в локальный кэш. Кэш обновляется +по умолчанию каждую секунду (настраиваемо, подтягиваются только новые записи). +Был вариант ещё вместо обновления по тикеру сделать триггер в бд и через +notify/listen постгреса добавлять новые записи в кэш. Было бы эффективнее, +но на данном этапе выглядит как оверинженеринг. + +Для продакшена не хватает метрик и алертов в сентри, но на это уже +времени тратить не хочется. diff --git a/adapters/inmemorycache/cache.go b/adapters/inmemorycache/cache.go new file mode 100644 index 0000000..0693a9b --- /dev/null +++ b/adapters/inmemorycache/cache.go @@ -0,0 +1,11 @@ +package inmemorycache + +type Cache map[uint64][]string + +func (c Cache) Get(id uint64) []string { + return c[id] +} + +func (c Cache) Append(id uint64, ip string) { + c[id] = append(c[id], ip) +} diff --git a/application/application.go b/application/application.go new file mode 100644 index 0000000..34270a3 --- /dev/null +++ b/application/application.go @@ -0,0 +1,69 @@ +package application + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/uptrace/bun" + "go.uber.org/zap" + + "git.derfenix.pro/fenix/protect_trans_info/adapters/inmemorycache" + "git.derfenix.pro/fenix/protect_trans_info/application/repository" + "git.derfenix.pro/fenix/protect_trans_info/port/http" +) + +func NewApplication(ctx context.Context, cfg Config, logger *zap.Logger) (*Application, error) { + db, err := repository.NewDB(cfg.DB) + if err != nil { + return nil, fmt.Errorf("new db: %w", err) + } + + cache := make(inmemorycache.Cache) + + repo, err := repository.NewConnLogs(ctx, db, cache, logger, cfg.UpdateInterval) + if err != nil { + return nil, fmt.Errorf("conn log repo: %w", err) + } + + service, err := http.NewService(cfg.HTTPHost, cfg.HTTPPort, repo, logger) + if err != nil { + return nil, fmt.Errorf("new service: %w", err) + } + + return &Application{ + cfg: cfg, + service: service, + db: db, + }, nil +} + +type Application struct { + cfg Config + service *http.Service + db *bun.DB +} + +func (a *Application) Start(wg *sync.WaitGroup) { + wg.Add(1) + + go func() { + defer wg.Done() + + a.service.Start() + }() +} + +func (a *Application) Stop(wg *sync.WaitGroup) { + wg.Add(1) + + go func() { + defer wg.Done() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + a.service.Stop(ctx) + }() +} diff --git a/application/config.go b/application/config.go new file mode 100644 index 0000000..a027466 --- /dev/null +++ b/application/config.go @@ -0,0 +1,27 @@ +package application + +import ( + "context" + "fmt" + "time" + + "github.com/sethvargo/go-envconfig" +) + +func NewConfig() (Config, error) { + cfg := Config{} + + if err := envconfig.Process(context.Background(), &cfg); err != nil { + return Config{}, fmt.Errorf("envconfig process: %w", err) + } + + return cfg, nil +} + +type Config struct { + Devel bool `env:"DEVEL"` + HTTPPort uint `env:"HTTP_PORT, default=8001"` + HTTPHost string `env:"HTTP_HOST,default=0.0.0.0"` + DB string `env:"DB_DSN"` + UpdateInterval time.Duration `env:"UPDATE_INTERVAL,default=1s"` +} diff --git a/application/migration.go b/application/migration.go new file mode 100644 index 0000000..13461e5 --- /dev/null +++ b/application/migration.go @@ -0,0 +1,37 @@ +package application + +import ( + "context" + "embed" + "fmt" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" + "go.uber.org/zap" +) + +//go:embed migrations/*.sql +var migrationFiles embed.FS + +func Migrate(ctx context.Context, db *bun.DB, logger *zap.Logger) error { + migrations := migrate.NewMigrations() + + if err := migrations.Discover(migrationFiles); err != nil { + return fmt.Errorf("discover migrations: %w", err) + } + + migrator := migrate.NewMigrator(db, migrations) + + if initErr := migrator.Init(ctx); initErr != nil { + return fmt.Errorf("init migrations: %w", initErr) + } + + group, err := migrator.Migrate(ctx) + if err != nil { + return fmt.Errorf("migrate: %w", err) + } + + logger.Sugar().Infof("migrated: %s", group.String()) + + return nil +} diff --git a/application/migrations/20230824180522_conn_logs_table.up.sql b/application/migrations/20230824180522_conn_logs_table.up.sql new file mode 100644 index 0000000..a6622db --- /dev/null +++ b/application/migrations/20230824180522_conn_logs_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "conn_log" +( + "user_id" BIGINT, + "ip_addr" VARCHAR(15), + "ts" timestamp +); +--bun:split + +CREATE INDEX "conn_log_user_ip_idx" ON "conn_log" ("ip_addr", "user_id"); +--bun:split + +CREATE INDEX "conn_log_ip_idx" ON "conn_log" ("ip_addr"); +--bun:split + +CREATE INDEX "conn_log_ts_idx" ON "conn_log" ("ts"); diff --git a/application/repository/bunpg.go b/application/repository/bunpg.go new file mode 100644 index 0000000..0052120 --- /dev/null +++ b/application/repository/bunpg.go @@ -0,0 +1,23 @@ +package repository + +import ( + "database/sql" + "fmt" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" +) + +func NewDB(dsn string) (*bun.DB, error) { + connector := pgdriver.NewConnector(pgdriver.WithDSN(dsn)) + sqlDB := sql.OpenDB(connector) + sqlDB.SetMaxOpenConns(10) + db := bun.NewDB(sqlDB, pgdialect.New()) + + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("ping database: %w", err) + } + + return db, nil +} diff --git a/application/repository/logs.go b/application/repository/logs.go new file mode 100644 index 0000000..7fb3a6b --- /dev/null +++ b/application/repository/logs.go @@ -0,0 +1,142 @@ +package repository + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/uptrace/bun" + "go.uber.org/zap" +) + +type Cache interface { + Get(id uint64) []string + Append(id uint64, ip string) +} + +type ConnLog struct { + bun.BaseModel `bun:"table:conn_log"` + UserID uint64 `bun:"user_id"` + IP string `bun:"ip_addr"` + TS time.Time `bun:"ts"` +} + +func NewConnLogs(ctx context.Context, db *bun.DB, cache Cache, logger *zap.Logger, updateInterval time.Duration) (*ConnLogs, error) { + connLogs := &ConnLogs{db: db, cache: cache} + + logger.Info("filling initial cache") + err := connLogs.fillCache(ctx) + if err != nil { + return nil, err + } + logger.Info("initial cache filled") + + go func() { + ticker := time.NewTicker(updateInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + var err error + + err = connLogs.fillCache(ctx) + if err != nil { + logger.Error("update cache", zap.Error(err)) + } + } + } + }() + + return connLogs, nil +} + +type ConnLogs struct { + db *bun.DB + mu sync.RWMutex + cache Cache + lastTS time.Time +} + +func (l *ConnLogs) fillCache(ctx context.Context) error { + var entity []ConnLog + + l.mu.Lock() + defer l.mu.Unlock() + + query := l.db.NewSelect().Model(&entity). + Order("ts"). + Group("user_id", "ip_addr"). + Column("user_id", "ip_addr"). + ColumnExpr(`max("ts") as ts`) + + if !l.lastTS.IsZero() { + query.Where(`"ts" > ? `, l.lastTS) + } + + if err := query.Scan(ctx); err != nil { + return fmt.Errorf("select: %w", err) + } + +loop: + for i := range entity { + item := &entity[i] + + if ips := l.cache.Get(item.UserID); len(ips) == 0 { + l.cache.Append(item.UserID, item.IP) + continue + } + + for _, s := range l.cache.Get(item.UserID) { + if s == item.IP { + continue loop + } + } + + l.cache.Append(item.UserID, item.IP) + } + + if len(entity) > 0 { + l.lastTS = entity[len(entity)-1].TS + } + + return nil +} + +func (l *ConnLogs) Get(_ context.Context, first, second uint64) (bool, error) { + ips1 := l.cache.Get(first) + ips2 := l.cache.Get(second) + + if len(ips1) == 0 || len(ips2) == 0 { + return false, nil + } + + for i := range ips1 { + for j := range ips2 { + if ips1[i] == ips2[j] { + return true, nil + } + } + } + + return false, nil +} + +func (l *ConnLogs) List(ctx context.Context) (string, error) { + var entity []ConnLog + + if err := l.db.NewSelect().Model(&entity).Scan(ctx); err != nil { + return "", fmt.Errorf("select: %w", err) + } + + marshal, err := json.Marshal(entity) + if err != nil { + return "", fmt.Errorf("marshal: %w", err) + } + + return string(marshal), nil +} diff --git a/application/repository/logs_test.go b/application/repository/logs_test.go new file mode 100644 index 0000000..e0ae95d --- /dev/null +++ b/application/repository/logs_test.go @@ -0,0 +1,151 @@ +package repository_test + +import ( + "context" + "database/sql" + "fmt" + "testing" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" + "go.uber.org/zap/zaptest" + + "git.derfenix.pro/fenix/protect_trans_info/adapters/inmemorycache" + "git.derfenix.pro/fenix/protect_trans_info/application" + . "git.derfenix.pro/fenix/protect_trans_info/application/repository" +) + +func TestLogs_Get(t *testing.T) { + if testing.Short() { + t.Skip("skip long test") + } + + t.Parallel() + + dockerPool, err := dockertest.NewPool("") + dockerPool.MaxWait = time.Second * 10 + require.NoError(t, err) + + resource, err := dockerPool.RunWithOptions(&dockertest.RunOptions{ + Repository: "postgres", + Tag: "15", + Env: []string{ + "POSTGRES_USER=test", + "POSTGRES_PASSWORD=test", + "POSTGRES_DB=test", + "POSTGRES_HOST_AUTH_METHOD=md5", + "POSTGRES_INITDB_ARGS=--auth-host=md5", + }, + PortBindings: map[docker.Port][]docker.PortBinding{"5432/tcp": {{HostIP: "0.0.0.0", HostPort: "55432"}}}, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + }) + require.NoError(t, err) + + t.Cleanup(func() { + err := dockerPool.Purge(resource) + assert.NoError(t, err) + }) + + connector := pgdriver.NewConnector(pgdriver.WithDSN("postgresql://test:test@localhost:55432/test?sslmode=disable")) + sqlDB := sql.OpenDB(connector) + sqlDB.SetMaxOpenConns(10) + db := bun.NewDB(sqlDB, pgdialect.New()) + + err = dockerPool.Retry(func() error { + pingCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + if err := db.PingContext(pingCtx); err != nil { + return fmt.Errorf("ping database: %w", err) + } + + return nil + }) + require.NoError(t, err) + + ctx := context.Background() + logger := zaptest.NewLogger(t) + + require.NoError(t, application.Migrate(ctx, db, logger)) + + testData := []ConnLog{ + { + UserID: 1, + IP: "123.123.123.123", + TS: time.Now(), + }, + { + UserID: 2, + IP: "123.123.123.123", + TS: time.Now().Add(time.Hour), + }, + { + UserID: 3, + IP: "124.123.123.123", + TS: time.Now().Add(time.Hour * 2), + }, + } + + _, err = db.NewInsert().Model(&testData).Exec(ctx) + require.NoError(t, err) + + repo, err := NewConnLogs(ctx, db, make(inmemorycache.Cache), logger, time.Millisecond*100) + require.NoError(t, err) + + t.Run("found dup", func(t *testing.T) { + t.Parallel() + + get, err := repo.Get(ctx, 1, 2) + require.NoError(t, err) + require.True(t, get) + }) + + t.Run("no dup 1", func(t *testing.T) { + t.Parallel() + + get, err := repo.Get(ctx, 1, 3) + require.NoError(t, err) + require.False(t, get) + }) + + t.Run("no dup 2", func(t *testing.T) { + t.Parallel() + + get, err := repo.Get(ctx, 2, 3) + require.NoError(t, err) + require.False(t, get) + }) + + t.Run("added item", func(t *testing.T) { + t.Parallel() + + get, err := repo.Get(ctx, 4, 3) + require.NoError(t, err) + require.False(t, get) + + _, err = db.NewInsert().Model(&ConnLog{ + BaseModel: bun.BaseModel{}, + UserID: 4, + IP: "124.123.123.123", + TS: time.Now().Add(time.Hour * 3), + }).Exec(ctx) + require.NoError(t, err) + + get, err = repo.Get(ctx, 4, 3) + require.NoError(t, err) + assert.False(t, get) + + time.Sleep(time.Millisecond * 200) + + get, err = repo.Get(ctx, 4, 3) + require.NoError(t, err) + require.True(t, get) + }) +} diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..66fe42e --- /dev/null +++ b/bench_test.go @@ -0,0 +1,30 @@ +package protect_trans_info + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func BenchmarkDup(b *testing.B) { + c := http.Client{} + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8001/43/333", nil) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, err := c.Do(req) + require.NoError(b, err) + } +} + +func FuzzDup(f *testing.F) { + f.Fuzz(func(t *testing.T, firstID uint16, secondID uint16) { + c := http.Client{} + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://127.0.0.1:8001/%d/%d", firstID, secondID), nil) + require.NoError(t, err) + _, err = c.Do(req) + require.NoError(t, err) + }) +} diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go new file mode 100644 index 0000000..7c3614d --- /dev/null +++ b/cmd/migrate/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "fmt" + "os/signal" + + "go.uber.org/zap" + + "git.derfenix.pro/fenix/protect_trans_info/application" + "git.derfenix.pro/fenix/protect_trans_info/application/repository" +) + +func main() { + cfg, err := application.NewConfig() + if err != nil { + panic(fmt.Sprintf("load config: %v", err)) + } + + db, err := repository.NewDB(cfg.DB) + if err != nil { + panic(fmt.Sprintf("new db: %v", err)) + } + + logger, err := zap.NewProduction() + if err != nil { + panic(fmt.Sprintf("new logger: %v", err)) + } + + ctx, cancel := signal.NotifyContext(context.Background()) + defer cancel() + + if err := application.Migrate(ctx, db, logger); err != nil { + logger.Fatal("migrate failed", zap.Error(err)) + } +} diff --git a/cmd/seed/main.go b/cmd/seed/main.go new file mode 100644 index 0000000..95bb665 --- /dev/null +++ b/cmd/seed/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + "git.derfenix.pro/fenix/protect_trans_info/application" + "git.derfenix.pro/fenix/protect_trans_info/application/repository" + "git.derfenix.pro/fenix/protect_trans_info/scripts" +) + +func main() { + cfg, err := application.NewConfig() + if err != nil { + panic(fmt.Sprintf("load config: %v", err)) + } + + db, err := repository.NewDB(cfg.DB) + if err != nil { + panic(fmt.Sprintf("new db: %v", err)) + } + + logger, err := zap.NewProduction() + if err != nil { + panic(fmt.Sprintf("new logger: %v", err)) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*3) + defer cancel() + + logger.Info("start seeding") + + if err := scripts.SeedData(ctx, db, logger); err != nil { + logger.Fatal("failed to seed data", zap.Error(err)) + } + + logger.Info("stop seeding") +} diff --git a/cmd/service/main.go b/cmd/service/main.go new file mode 100644 index 0000000..7af4af6 --- /dev/null +++ b/cmd/service/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + + "go.uber.org/zap" + + "git.derfenix.pro/fenix/protect_trans_info/application" +) + +func main() { + cfg, err := application.NewConfig() + if err != nil { + panic(fmt.Sprintf("load config: %v", err)) + } + + var logger *zap.Logger + + switch cfg.Devel { + case true: + logger, err = zap.NewDevelopment() + case false: + logger, err = zap.NewProduction() + } + if err != nil { + panic(fmt.Sprintf("init logger: %v", err)) + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) + defer cancel() + + app, err := application.NewApplication(ctx, cfg, logger) + if err != nil { + logger.Fatal("create application failed", zap.Error(err)) + } + + wg := sync.WaitGroup{} + + go func() { + <-ctx.Done() + app.Stop(&wg) + }() + + app.Start(&wg) + + wg.Wait() + + // Context did not stop the application + if ctx.Err() == nil { + os.Exit(2) + } +} diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..2bc47c0 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.21 as builder + +WORKDIR /project + +COPY go.* . +RUN go get ./... + +COPY . . + +RUN export CGO_ENABLED=0 && go build -o service ./cmd/service/main.go &&\ + go build -o seed ./cmd/seed/main.go && \ + go build -o migrate ./cmd/migrate/main.go + +FROM alpine:latest + +COPY --from=builder /project/service /service +COPY --from=builder /project/seed /seed +COPY --from=builder /project/migrate /migrate diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..08aeb64 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,49 @@ +version: "3" + +volumes: + db: + +services: + base: + build: + dockerfile: deploy/Dockerfile + context: . + db: + image: postgres:15 + environment: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: test + POSTGRES_HOST_AUTH_METHOD: md5 + POSTGRES_INITDB_ARGS: --auth-host=md5 + service: + extends: + service: base + environment: + DB_DSN: "postgresql://test:test@db:5432/test?sslmode=disable" + PORT: 8001 + DEVEL: true + ports: + - "8001:8001" + command: + - /service + depends_on: + - db + migrate: + extends: + service: base + environment: + DB_DSN: "postgresql://test:test@db:5432/test?sslmode=disable" + command: + - /migrate + depends_on: + - db + seed: + extends: + service: base + environment: + DB_DSN: "postgresql://test:test@db:5432/test?sslmode=disable" + command: + - /seed + depends_on: + - db diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5171341 --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module git.derfenix.pro/fenix/protect_trans_info + +go 1.21 + +require ( + github.com/go-chi/chi/v5 v5.0.10 + github.com/ory/dockertest/v3 v3.10.0 + github.com/sethvargo/go-envconfig v0.9.0 + github.com/stretchr/testify v1.8.1 + github.com/uptrace/bun v1.1.14 + github.com/uptrace/bun/dialect/pgdialect v1.1.14 + github.com/uptrace/bun/driver/pgdriver v1.1.14 + go.uber.org/zap v1.25.0 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + mellium.im/sasl v0.3.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c7cc7a8 --- /dev/null +++ b/go.sum @@ -0,0 +1,198 @@ +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= +github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM= +github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8= +github.com/uptrace/bun/dialect/pgdialect v1.1.14 h1:b7+V1KDJPQSFYgkG/6YLXCl2uvwEY3kf/GSM7hTHRDY= +github.com/uptrace/bun/dialect/pgdialect v1.1.14/go.mod h1:v6YiaXmnKQ2FlhRD2c0ZfKd+QXH09pYn4H8ojaavkKk= +github.com/uptrace/bun/driver/pgdriver v1.1.14 h1:V2Etm7mLGS3mhx8ddxZcUnwZLX02Jmq9JTlo0sNVDhA= +github.com/uptrace/bun/driver/pgdriver v1.1.14/go.mod h1:D4FjWV9arDYct6sjMJhFoyU71SpllZRHXFRRP2Kd0Kw= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= +mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= diff --git a/port/http/service.go b/port/http/service.go new file mode 100644 index 0000000..6818b73 --- /dev/null +++ b/port/http/service.go @@ -0,0 +1,144 @@ +package http + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "go.uber.org/zap" +) + +type LogStorage interface { + Get(ctx context.Context, first, second uint64) (bool, error) + List(ctx context.Context) (string, error) +} + +type Response struct { + Dupes *bool `json:"dupes,omitempty"` + Err string `json:"error,omitempty"` +} + +func NewService(host string, port uint, log LogStorage, logger *zap.Logger) (*Service, error) { + srv := http.Server{ + Addr: fmt.Sprintf("%s:%d", host, port), + ReadTimeout: time.Second, + ReadHeaderTimeout: time.Millisecond * 500, + WriteTimeout: time.Second * 10, + IdleTimeout: time.Minute, + MaxHeaderBytes: 1024 * 16, + } + + service := Service{ + server: &srv, + log: log, + logger: logger, + } + + mux := chi.NewMux() + mux.Use(middleware.StripSlashes) + + mux.Get("/{firstID:[0-9]+}/{secondID:[0-9]+}", service.handleDup) + mux.Get("/list", service.List) + + srv.Handler = mux + + return &service, nil +} + +type Service struct { + server *http.Server + log LogStorage + logger *zap.Logger +} + +func (s *Service) Start() { + s.logger.Info("starting http service", zap.String("address", s.server.Addr)) + + if err := s.server.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("listen and serve failed", zap.Error(err)) + } + } +} + +func (s *Service) Stop(ctx context.Context) { + if err := s.server.Shutdown(ctx); err != nil { + s.logger.Error("the graceful shutdown has failed", zap.Error(err)) + } +} + +func (s *Service) handleDup(w http.ResponseWriter, r *http.Request) { + started := time.Now() + + firstIDStr := chi.URLParam(r, "firstID") + firstID, err := strconv.ParseUint(firstIDStr, 10, 64) + if err != nil { + s.badRequest(w, Response{Err: fmt.Sprintf("invalid first id value: %s", firstIDStr)}) + + return + } + + secondIDStr := chi.URLParam(r, "secondID") + secondID, err := strconv.ParseUint(secondIDStr, 10, 64) + if err != nil { + s.badRequest(w, Response{Err: fmt.Sprintf("invalid second id value: %s", secondIDStr)}) + + return + } + + logger := s.logger.With(zap.Uint64("first_id", firstID), zap.Uint64("second_id", secondID)) + logger.Debug("new dup request") + + if secondID == firstID { + t := true + s.writeResponse(w, Response{Dupes: &t}, http.StatusOK) + + return + } + + result, err := s.log.Get(r.Context(), firstID, secondID) + if err != nil { + logger.Error("log storage get failed", zap.Error(err)) + s.internalError(w, Response{Err: fmt.Sprintf("storage error")}) + + return + } + + logger.Debug("result", zap.Bool("dup", result)) + + w.Header().Add("Server-Timing", fmt.Sprintf("db;dur=%dus", time.Since(started).Microseconds())) + s.writeResponse(w, Response{Dupes: &result}, http.StatusOK) +} + +func (s *Service) badRequest(w http.ResponseWriter, response Response) { + s.writeResponse(w, response, http.StatusBadRequest) +} + +func (s *Service) internalError(w http.ResponseWriter, response Response) { + s.writeResponse(w, response, http.StatusInternalServerError) +} + +func (s *Service) writeResponse(w http.ResponseWriter, response Response, code int) { + w.WriteHeader(code) + err := json.NewEncoder(w).Encode(response) + if err != nil { + s.logger.Error("encode bad request response", zap.Error(err), zap.Any("response", response)) + } +} + +func (s *Service) List(w http.ResponseWriter, r *http.Request) { + list, err := s.log.List(r.Context()) + if err != nil { + s.internalError(w, Response{Err: err.Error()}) + + return + } + + _, _ = w.Write([]byte(list)) +} diff --git a/scripts/seed.go b/scripts/seed.go new file mode 100644 index 0000000..0f424b5 --- /dev/null +++ b/scripts/seed.go @@ -0,0 +1,75 @@ +package scripts + +import ( + "context" + "encoding/binary" + "fmt" + "math/rand" + "net" + "time" + + "github.com/uptrace/bun" + "go.uber.org/zap" + + "git.derfenix.pro/fenix/protect_trans_info/application/repository" +) + +func SeedData(ctx context.Context, db *bun.DB, logger *zap.Logger) error { + now := time.Now().Add(-24 * time.Hour) + + const bufSize = 500 + buf := make([]repository.ConnLog, 0, bufSize) + + for i := 0; i < 2_000_000; i++ { + buf = append(buf, repository.ConnLog{ + UserID: uint64(rand.Int63n(10000)), + IP: randomIP(), + TS: now.Add(time.Second * time.Duration(i)), + }) + + if len(buf) == bufSize { + _, err := db.NewInsert().Model(&buf).Exec(ctx) + if err != nil { + return fmt.Errorf("insert bulk: %w", err) + } + logger.Sugar().Infof("insert %d items", len(buf)) + + buf = make([]repository.ConnLog, 0, bufSize) + } + } + + if len(buf) > 0 { + _, err := db.NewInsert().Model(&buf).Exec(ctx) + if err != nil { + return fmt.Errorf("insert bulk: %w", err) + } + logger.Sugar().Infof("insert %d items", len(buf)) + } + + fixedDup := []repository.ConnLog{ + repository.ConnLog{ + UserID: 88888, + IP: "127.0.0.1", + TS: now.Add(time.Second * time.Duration(888)), + }, + repository.ConnLog{ + UserID: 99999, + IP: "127.0.0.1", + TS: now.Add(time.Second * time.Duration(999)), + }, + } + + _, err := db.NewInsert().Model(&fixedDup).Exec(ctx) + if err != nil { + return fmt.Errorf("insert fixed dup: %w", err) + } + + return nil +} + +func randomIP() string { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, rand.Uint32()) + + return net.IP(buf).String() +} diff --git a/test.http b/test.http new file mode 100644 index 0000000..78b6978 --- /dev/null +++ b/test.http @@ -0,0 +1,7 @@ +GET http://127.0.0.1:8001/12/233/ + +### +GET http://127.0.0.1:8001/88888/99999 + +### +GET http://127.0.0.1:8001/list diff --git a/testdata/fuzz/FuzzDup/149d6ad96b2f24d6 b/testdata/fuzz/FuzzDup/149d6ad96b2f24d6 new file mode 100644 index 0000000..fcbf603 --- /dev/null +++ b/testdata/fuzz/FuzzDup/149d6ad96b2f24d6 @@ -0,0 +1,3 @@ +go test fuzz v1 +uint16(91) +uint16(10) diff --git a/testdata/fuzz/FuzzDup/29aa5edb5a37da5e b/testdata/fuzz/FuzzDup/29aa5edb5a37da5e new file mode 100644 index 0000000..e161229 --- /dev/null +++ b/testdata/fuzz/FuzzDup/29aa5edb5a37da5e @@ -0,0 +1,3 @@ +go test fuzz v1 +uint16(93) +uint16(108) diff --git a/testdata/fuzz/FuzzDup/642e3858300a23d4 b/testdata/fuzz/FuzzDup/642e3858300a23d4 new file mode 100644 index 0000000..c24fae0 --- /dev/null +++ b/testdata/fuzz/FuzzDup/642e3858300a23d4 @@ -0,0 +1,3 @@ +go test fuzz v1 +uint16(4) +uint16(37)