diff --git a/build.docker b/build.docker index 5dc4cec..1b65e25 100644 --- a/build.docker +++ b/build.docker @@ -1,7 +1,7 @@ FROM golang:1.17 RUN apt-get update && apt-get install -y \ - rsync libssl-dev postgresql sudo \ + rsync libssl-dev postgresql sudo redis-server \ && rm -rf /var/lib/apt/lists/* RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0 diff --git a/shopfront/README.md b/shopfront/README.md new file mode 100644 index 0000000..18959b5 --- /dev/null +++ b/shopfront/README.md @@ -0,0 +1,21 @@ +# shopfront + +В этой задаче вам нужно реализовать хранилище счётчиков посещений поверх redis. + +- Метод `RecordView` запоминает, что пользователь посетил страницу `item`-а. +- Метод `GetItems` загружает счётчики для пачки `item` ов. В поле `item[i].Viewed` должен + быть записан флаг, означающий что пользователь посетил `i`-ый `item`. + +В этой задаче есть benchmark-и. Чтобы пройти его, ваше решение должно использовать [pipelining](https://redis.io/docs/manual/pipelining/). + +## Запуск тестов на linux + +Для работы тестов на ubuntu нужно установить пакет `redis-server`. + +``` +sudo apt install redis-server +``` + +Если вы работаете на другом дистрибутиве linux, воспользуйтесь своим пакетным менеджером. + +Тесты сами запускают `redis` в начале, и останавливают его в конце. diff --git a/shopfront/model.go b/shopfront/model.go new file mode 100644 index 0000000..0f129d2 --- /dev/null +++ b/shopfront/model.go @@ -0,0 +1,19 @@ +package shopfront + +import "context" + +type ( + ItemID int64 + UserID int64 +) + +type Item struct { + ViewCount int + Viewed bool +} + +type Counters interface { + GetItems(ctx context.Context, ids []ItemID, userID UserID) ([]Item, error) + + RecordView(ctx context.Context, id ItemID, userID UserID) error +} diff --git a/shopfront/redis_test.go b/shopfront/redis_test.go new file mode 100644 index 0000000..20aecfd --- /dev/null +++ b/shopfront/redis_test.go @@ -0,0 +1,56 @@ +package shopfront_test + +import ( + "os" + "os/exec" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/slon/shad-go/tools/testtool" +) + +type testingTB interface { + Logf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + FailNow() + Cleanup(func()) +} + +func StartRedis(tb testingTB) string { + if redis, ok := os.LookupEnv("REDIS"); ok { + tb.Logf("using external redis server; REDIS=%s", redis) + return redis + } + + port, err := testtool.GetFreePort() + require.NoError(tb, err) + + _, err = exec.LookPath("redis-server") + if err != nil { + tb.Fatalf("redis-server binary is not found; is redis installed?") + } + + cmd := exec.Command("redis-server", "--port", port, "--save", "", "--appendonly", "no") + cmd.Stderr = os.Stderr + + require.NoError(tb, cmd.Start()) + + finished := make(chan error, 1) + go func() { + finished <- cmd.Wait() + }() + + select { + case err := <-finished: + tb.Fatalf("redis server terminated: %v", err) + + case <-time.After(time.Second / 2): + } + + tb.Cleanup(func() { + _ = cmd.Process.Kill() + }) + + return "localhost:" + port +} diff --git a/shopfront/shopfront.go b/shopfront/shopfront.go new file mode 100644 index 0000000..60b038f --- /dev/null +++ b/shopfront/shopfront.go @@ -0,0 +1,9 @@ +//go:build !solution + +package shopfront + +import "github.com/go-redis/redis/v8" + +func New(rdb *redis.Client) Counters { + panic("not implemented") +} diff --git a/shopfront/shopfront_test.go b/shopfront/shopfront_test.go new file mode 100644 index 0000000..c94b991 --- /dev/null +++ b/shopfront/shopfront_test.go @@ -0,0 +1,66 @@ +package shopfront_test + +import ( + "context" + "testing" + + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/require" + "gitlab.com/slon/shad-go/shopfront" +) + +func TestShopfront(t *testing.T) { + rdb := redis.NewClient(&redis.Options{ + Addr: StartRedis(t), + }) + + ctx := context.Background() + + c := shopfront.New(rdb) + + items, err := c.GetItems(ctx, []shopfront.ItemID{1, 2, 3, 4}, 42) + require.NoError(t, err) + require.Equal(t, items, []shopfront.Item{ + {}, + {}, + {}, + {}, + }) + + require.NoError(t, c.RecordView(ctx, 3, 42)) + require.NoError(t, c.RecordView(ctx, 2, 42)) + + require.NoError(t, c.RecordView(ctx, 2, 4242)) + + items, err = c.GetItems(ctx, []shopfront.ItemID{1, 2, 3, 4}, 42) + require.NoError(t, err) + require.Equal(t, items, []shopfront.Item{ + {}, + {ViewCount: 2, Viewed: true}, + {ViewCount: 1, Viewed: true}, + {}, + }) +} + +func BenchmarkShopfront(b *testing.B) { + const nItems = 1024 + + rdb := redis.NewClient(&redis.Options{ + Addr: StartRedis(b), + }) + + ctx := context.Background() + c := shopfront.New(rdb) + + var ids []shopfront.ItemID + for i := 0; i < nItems; i++ { + ids = append(ids, shopfront.ItemID(i)) + require.NoError(b, c.RecordView(ctx, shopfront.ItemID(i), 42)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := c.GetItems(ctx, ids, 42) + require.NoError(b, err) + } +}