Merge branch 'master' of gitlab.com:slon/shad-go-private into treeiter
This commit is contained in:
commit
6613189930
157 changed files with 1264 additions and 198 deletions
|
@ -1,6 +1,99 @@
|
||||||
|
- group: SQL
|
||||||
|
start: 31-03-2022 18:00
|
||||||
|
deadline: 17-04-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: wscat
|
||||||
|
score: 200
|
||||||
|
- task: dao
|
||||||
|
score: 100
|
||||||
|
- task: ledger
|
||||||
|
score: 200
|
||||||
|
- task: shopfront
|
||||||
|
score: 100
|
||||||
|
|
||||||
|
- group: HTTP
|
||||||
|
start: 23-03-2022 18:00
|
||||||
|
deadline: 10-04-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: urlshortener
|
||||||
|
score: 100
|
||||||
|
- task: digitalclock
|
||||||
|
score: 100
|
||||||
|
- task: olympics
|
||||||
|
score: 200
|
||||||
|
- task: firewall
|
||||||
|
score: 200
|
||||||
|
- task: coverme
|
||||||
|
score: 300
|
||||||
|
|
||||||
|
- group: Concurrency with shared memory
|
||||||
|
start: 17-03-2022 23:59
|
||||||
|
deadline: 03-04-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: dupcall
|
||||||
|
score: 200
|
||||||
|
- task: keylock
|
||||||
|
score: 200
|
||||||
|
- task: batcher
|
||||||
|
score: 200
|
||||||
|
- task: pubsub
|
||||||
|
score: 300
|
||||||
|
|
||||||
|
- group: "[HW] Gitfame"
|
||||||
|
start: 11-03-2022 11:59
|
||||||
|
deadline: 30-03-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: gitfame
|
||||||
|
score: 0
|
||||||
|
|
||||||
|
- group: Testing
|
||||||
|
start: 10-03-2022 20:00
|
||||||
|
deadline: 29-03-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: testequal
|
||||||
|
score: 100
|
||||||
|
- task: fileleak
|
||||||
|
score: 100
|
||||||
|
- task: tabletest
|
||||||
|
score: 100
|
||||||
|
- task: tparallel
|
||||||
|
score: 200
|
||||||
|
|
||||||
|
- group: Goroutines
|
||||||
|
start: 03-03-2022 18:00
|
||||||
|
deadline: 22-03-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: tour1
|
||||||
|
score: 100
|
||||||
|
- task: once
|
||||||
|
score: 100
|
||||||
|
- task: rwmutex
|
||||||
|
score: 100
|
||||||
|
- task: waitgroup
|
||||||
|
score: 100
|
||||||
|
- task: cond
|
||||||
|
score: 100
|
||||||
|
- task: ratelimit
|
||||||
|
score: 100
|
||||||
|
|
||||||
|
- group: Interfaces
|
||||||
|
start: 24-02-2022 18:00
|
||||||
|
deadline: 13-03-2022 23:59
|
||||||
|
tasks:
|
||||||
|
- task: otp
|
||||||
|
score: 100
|
||||||
|
- task: lrucache
|
||||||
|
score: 100
|
||||||
|
- task: externalsort
|
||||||
|
score: 100
|
||||||
|
- task: retryupdate
|
||||||
|
score: 100
|
||||||
|
- task: ciletters
|
||||||
|
score: 100
|
||||||
|
|
||||||
- group: Basics
|
- group: Basics
|
||||||
start: 17-02-2022 18:00
|
start: 17-02-2022 18:00
|
||||||
deadline: 27-02-2022 23:59
|
deadline: 06-03-2022 23:59
|
||||||
tasks:
|
tasks:
|
||||||
- task: hotelbusiness
|
- task: hotelbusiness
|
||||||
score: 100
|
score: 100
|
||||||
|
|
|
@ -43,6 +43,13 @@ linters-settings:
|
||||||
stylecheck:
|
stylecheck:
|
||||||
# https://staticcheck.io/docs/options#checks
|
# https://staticcheck.io/docs/options#checks
|
||||||
checks: ["all", "-ST1018"]
|
checks: ["all", "-ST1018"]
|
||||||
|
# https://staticcheck.io/docs/options#initialisms
|
||||||
|
initialisms: [
|
||||||
|
"ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS",
|
||||||
|
"RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL",
|
||||||
|
"UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "CSV",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
|
|
|
@ -11,8 +11,8 @@ check:
|
||||||
- golangci-lint run --build-tags private,solution ./...
|
- golangci-lint run --build-tags private,solution ./...
|
||||||
#- go test -v -tags private,solution ./...
|
#- go test -v -tags private,solution ./...
|
||||||
#- go test -v -race -tags private,solution ./...
|
#- go test -v -race -tags private,solution ./...
|
||||||
- go test $(go list -tags private,solution ./... | grep -v /dockertest | grep -v /foolsday3) -tags private,solution -v ./...
|
- sudo -u nobody HOME=/tmp PATH=$PATH /usr/local/go/bin/go test $(go list -tags private,solution ./... | grep -v /dockertest | grep -v /foolsday3) -tags private,solution -v ./...
|
||||||
- go test $(go list -tags private,solution ./... | grep -v /dockertest) -tags private,solution -v -race ./...
|
- sudo -u nobody HOME=/tmp PATH=$PATH /usr/local/go/bin/go test $(go list -tags private,solution ./... | grep -v /dockertest) -tags private,solution -v -race ./...
|
||||||
|
|
||||||
rebuild-base-image:
|
rebuild-base-image:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Курс по Го в ШАД
|
# Курс по Го в ШАД
|
||||||
|
|
||||||
Для работы с кодом нужен go 1.17 или выше.
|
Для работы с кодом нужен go 1.18 или выше.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://gitlab.com/slon/shad-go.git
|
git clone https://gitlab.com/slon/shad-go.git
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
`Counter` используется для нахождения уникальных слов и подсчета вхождений каждого из них.
|
`Counter` используется для нахождения уникальных слов и подсчета вхождений каждого из них.
|
||||||
Его интерфейс выглядит так:
|
Его интерфейс выглядит так:
|
||||||
|
|
||||||
* `Count(r io.Reader) error` — функция, которая подсчитывает количество вхождений для каждого слова в тексте.
|
* `Count(r io.Reader) error` — функция, которая подсчитывает количество вхождений каждого слова в тексте.
|
||||||
На вход подается io.Reader, в котором находится некоторый текст.
|
На вход подается io.Reader, в котором находится некоторый текст.
|
||||||
Разделителями являются только переносы строк и пробелы.
|
Разделителями являются только переносы строк и пробелы.
|
||||||
* `String() string` — преобразует мапу вида `{"слово": "количество вхождений"}` в форматированную строку.
|
* `String() string` — преобразует мапу вида `{"слово": "количество вхождений"}` в форматированную строку.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package allocs
|
package allocs
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution && !change
|
//go:build !solution && !change
|
||||||
// +build !solution,!change
|
|
||||||
|
|
||||||
package allocs
|
package allocs
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package batcher
|
package batcher
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build race
|
//go:build race
|
||||||
// +build race
|
|
||||||
|
|
||||||
package batcher
|
package batcher
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package slow
|
package slow
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package blowfish
|
package blowfish
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
FROM golang:1.17
|
FROM golang:1.18
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
rsync libssl-dev \
|
rsync libssl-dev postgresql sudo redis-server \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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
|
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2
|
||||||
RUN curl -L "https://github.com/docker/compose/releases/download/1.28.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
RUN curl -L "https://github.com/docker/compose/releases/download/1.28.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
RUN chmod +x /usr/local/bin/docker-compose
|
RUN chmod +x /usr/local/bin/docker-compose
|
||||||
RUN curl -fsSL https://get.docker.com | sh
|
RUN curl -fsSL https://get.docker.com | sh
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
## ciletters
|
## ciletters
|
||||||
|
|
||||||
В этой задаче вам предстоит познакомиться со стандартным пакетом [text/template](https://golang.org/pkg/text/template/),
|
В этой задаче вам предстоит познакомиться со стандартным пакетом [text/template](https://golang.org/pkg/text/template/),
|
||||||
позволяющим генерировать текст в определенном формате.
|
позволяющим генерировать текст в определенном формате, а также с пакетом [embed](https://pkg.go.dev/embed),
|
||||||
|
позволяющим вкомпиливать в исполняемый файл строковые литералы.
|
||||||
|
|
||||||
### Легенда
|
### Легенда
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@
|
||||||
Нужно реализовать функцию `MakeLetter` из файла [letter.go](./letter.go),
|
Нужно реализовать функцию `MakeLetter` из файла [letter.go](./letter.go),
|
||||||
которая по go объекту нотификации генерирует её текстовое представление.
|
которая по go объекту нотификации генерирует её текстовое представление.
|
||||||
|
|
||||||
|
Для этого нужно написать `text/template` шаблон, сохранить его в отдельный файл, а затем получить его содержимое в коде с помомщью `go:embed`.
|
||||||
|
|
||||||
#### Прокомментированный пример из теста
|
#### Прокомментированный пример из теста
|
||||||
```
|
```
|
||||||
Your pipeline #194613 has failed! // 194613 -- это ID pipeline'а
|
Your pipeline #194613 has failed! // 194613 -- это ID pipeline'а
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package ciletters
|
package ciletters
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package ciletters
|
package ciletters
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
### Что нужно сделать?
|
### Что нужно сделать?
|
||||||
|
|
||||||
Нужно написать реализацию Cond используя каналы.
|
Нужно написать реализацию Cond, используя каналы.
|
||||||
|
|
||||||
Использование пакета [sync](https://golang.org/pkg/sync) в этой задаче запрещено!
|
Использование пакета [sync](https://golang.org/pkg/sync) в этой задаче запрещено!
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package cond
|
package cond
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ func TestCondBroadcast(t *testing.T) {
|
||||||
|
|
||||||
// nolint
|
// nolint
|
||||||
func TestCondSignalStealing(t *testing.T) {
|
func TestCondSignalStealing(t *testing.T) {
|
||||||
for iters := 0; iters < 1000; iters++ {
|
for iters := 0; iters < 20000; iters++ {
|
||||||
var m sync.Mutex
|
var m sync.Mutex
|
||||||
cond := New(&m)
|
cond := New(&m)
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,18 @@ Package main можно не тестировать.
|
||||||
Coverage 100% говорит ровно о том, что все строки кода выполнялись.
|
Coverage 100% говорит ровно о том, что все строки кода выполнялись.
|
||||||
Хорошие тесты, в первую очередь, тестируют функциональность.
|
Хорошие тесты, в первую очередь, тестируют функциональность.
|
||||||
|
|
||||||
Как посмотреть coverage:
|
Как посмотреть общий coverage:
|
||||||
```
|
```
|
||||||
go test -v -cover ./coverme/...
|
go test -v -cover ./coverme/...
|
||||||
```
|
```
|
||||||
|
|
||||||
Coverage можно выводить в html (см. ссылки), и эта функциональность поддерживается в Goland.
|
Как посмотреть coverage пакета в html:
|
||||||
|
```
|
||||||
|
go test -v -coverprofile=/tmp/coverage.out ./coverme/models/...
|
||||||
|
go tool cover -html=/tmp/coverage.out
|
||||||
|
```
|
||||||
|
Аналогичная функциональность поддерживается в Goland.
|
||||||
|
Также рекомендуем ознакомиться с рассказом о cover в блоге (см. ссылки).
|
||||||
|
|
||||||
## Ссылки
|
## Ссылки
|
||||||
|
|
||||||
|
@ -44,7 +50,7 @@ Todo-app с минимальной функциональностью + client.
|
||||||
|
|
||||||
Health check:
|
Health check:
|
||||||
```
|
```
|
||||||
✗ curl -i -X GET localhost:6029/
|
✗ curl -i -X GET localhost:6029/
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Date: Thu, 19 Mar 2020 21:46:02 GMT
|
Date: Thu, 19 Mar 2020 21:46:02 GMT
|
||||||
|
@ -66,7 +72,7 @@ Content-Length: 51
|
||||||
|
|
||||||
Получить todo по id:
|
Получить todo по id:
|
||||||
```
|
```
|
||||||
✗ curl -i localhost:6029/todo/0
|
✗ curl -i localhost:6029/todo/0
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Date: Thu, 19 Mar 2020 21:44:17 GMT
|
Date: Thu, 19 Mar 2020 21:44:17 GMT
|
||||||
|
@ -77,7 +83,7 @@ Content-Length: 51
|
||||||
|
|
||||||
Получить все todo:
|
Получить все todo:
|
||||||
```
|
```
|
||||||
✗ curl -i -X GET localhost:6029/todo
|
✗ curl -i -X GET localhost:6029/todo
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Date: Thu, 19 Mar 2020 21:44:37 GMT
|
Date: Thu, 19 Mar 2020 21:44:37 GMT
|
||||||
|
@ -85,3 +91,19 @@ Content-Length: 53
|
||||||
|
|
||||||
[{"id":0,"title":"A","content":"a","finished":false}]
|
[{"id":0,"title":"A","content":"a","finished":false}]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Завершить todo:
|
||||||
|
```
|
||||||
|
✗ curl -i -X POST localhost:6029/todo/0/finish
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Date: Thu, 24 Mar 2022 15:40:49 GMT
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
✗ curl -i -X GET localhost:6029/todo
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Date: Thu, 24 Mar 2022 15:41:04 GMT
|
||||||
|
Content-Length: 52
|
||||||
|
|
||||||
|
[{"id":0,"title":"A","content":"a","finished":true}]%
|
||||||
|
```
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package app
|
package app
|
||||||
|
|
||||||
|
@ -9,7 +8,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
@ -34,10 +32,11 @@ func (app *App) Start(port int) {
|
||||||
|
|
||||||
func (app *App) initRoutes() {
|
func (app *App) initRoutes() {
|
||||||
app.router = mux.NewRouter()
|
app.router = mux.NewRouter()
|
||||||
app.router.HandleFunc("/", app.status).Methods("Get")
|
app.router.HandleFunc("/", app.status).Methods("GET")
|
||||||
app.router.HandleFunc("/todo", app.list).Methods("Get")
|
app.router.HandleFunc("/todo", app.list).Methods("GET")
|
||||||
app.router.HandleFunc("/todo/{id:[0-9]+}", app.getTodo).Methods("Get")
|
app.router.HandleFunc("/todo/{id:[0-9]+}", app.getTodo).Methods("GET")
|
||||||
app.router.HandleFunc("/todo/create", app.addTodo).Methods("Post")
|
app.router.HandleFunc("/todo/{id:[0-9]+}/finish", app.finishTodo).Methods("POST")
|
||||||
|
app.router.HandleFunc("/todo/create", app.addTodo).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) run(addr string) {
|
func (app *App) run(addr string) {
|
||||||
|
@ -80,7 +79,7 @@ func (app *App) addTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) getTodo(w http.ResponseWriter, r *http.Request) {
|
func (app *App) getTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/todo/"))
|
id, err := strconv.Atoi(mux.Vars(r)["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.BadRequest(w, "ID must be an int")
|
utils.BadRequest(w, "ID must be an int")
|
||||||
return
|
return
|
||||||
|
@ -95,6 +94,21 @@ func (app *App) getTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = utils.RespondJSON(w, http.StatusOK, todo)
|
_ = utils.RespondJSON(w, http.StatusOK, todo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) finishTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(mux.Vars(r)["id"])
|
||||||
|
if err != nil {
|
||||||
|
utils.BadRequest(w, "ID must be an int")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.db.FinishTodo(models.ID(id)); err != nil {
|
||||||
|
utils.ServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (app *App) status(w http.ResponseWriter, r *http.Request) {
|
func (app *App) status(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = utils.RespondJSON(w, http.StatusOK, "API is up and working!")
|
_ = utils.RespondJSON(w, http.StatusOK, "API is up and working!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package app
|
package app
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
|
@ -69,3 +68,19 @@ func (c *Client) List() ([]*models.Todo, error) {
|
||||||
err = json.NewDecoder(resp.Body).Decode(&todos)
|
err = json.NewDecoder(resp.Body).Decode(&todos)
|
||||||
return todos, err
|
return todos, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) Finish(id models.ID) error {
|
||||||
|
u := fmt.Sprintf("%s/todo/%d/finish", c.addr, id)
|
||||||
|
|
||||||
|
resp, err := http.Post(u, "application/json", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
@ -12,6 +11,7 @@ type Storage interface {
|
||||||
AddTodo(string, string) (*Todo, error)
|
AddTodo(string, string) (*Todo, error)
|
||||||
GetTodo(ID) (*Todo, error)
|
GetTodo(ID) (*Todo, error)
|
||||||
GetAll() ([]*Todo, error)
|
GetAll() ([]*Todo, error)
|
||||||
|
FinishTodo(ID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type InMemoryStorage struct {
|
type InMemoryStorage struct {
|
||||||
|
@ -69,3 +69,16 @@ func (s *InMemoryStorage) GetAll() ([]*Todo, error) {
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InMemoryStorage) FinishTodo(id ID) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
todo, ok := s.todos[id]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("todo %d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
todo.MarkFinished()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
|
|
47
dao/README.md
Normal file
47
dao/README.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# dao
|
||||||
|
|
||||||
|
В этой задаче вам нужно реализовать data access object для доступа к таблице пользователей.
|
||||||
|
|
||||||
|
- Функция `CreateDao` должна создавать таблицу в базе данных.
|
||||||
|
- Метод `Create` должен создавать нового пользователя и назначать его `ID`.
|
||||||
|
За генерацию `ID` должна отвечать база данных, использовать `rand` запрещается.
|
||||||
|
`pgx` не поддерживает `LastInsertId`, используйте синтаксис `RETURNING id`.
|
||||||
|
- Метод `Update` меняет `Name` пользователя.
|
||||||
|
- Метод `Delete` удаляет пользователя из таблицы.
|
||||||
|
- Метод `Lookup` возвращает `Name` пользователя по `ID`.
|
||||||
|
- Метод `List` возвращает список всех пользователей в таблице.
|
||||||
|
|
||||||
|
## Запуск тестов на linux
|
||||||
|
|
||||||
|
Для работы тестов на ubuntu нужно установить пакет `postgresql`.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
Если вы работаете на другом дистрибутиве linux, воспользуйтесь своим пакетным менеджером. Для работы тестов нужны исполняемые файлы `postgres` и `initdb`.
|
||||||
|
|
||||||
|
Тесты сами запускают базу данных в начале, и останавливают её в конце.
|
||||||
|
База данных работает от текущего пользователя во временной директории.
|
||||||
|
|
||||||
|
## Запуск postgres в docker
|
||||||
|
|
||||||
|
Даже если у вас linux (и уж тем более, если нет), вы можете не захотеть ставить postgres в систему.
|
||||||
|
Альтернативный способ — запустить бд в докере.
|
||||||
|
|
||||||
|
Для этого нужно установить docker и docker-compose по инструкции из [dockertest](../dockertest/README.md).
|
||||||
|
Добиться успешного запуска
|
||||||
|
```
|
||||||
|
go test -v ./dockertest/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Запускать тесты можно будет так:
|
||||||
|
```
|
||||||
|
(cd dao && docker-compose up -d && sleep 1 && env PGCONN="host=127.0.0.1 port=5432 database=shad-go user=gopher password=pass" go test -v ./... -count=1 || true && docker-compose down)
|
||||||
|
```
|
||||||
|
Эта команда стартует docker с postgresql, запускает тесты, передав им DSN через переменную окружения, удаляет контейнеры в конце.
|
||||||
|
|
||||||
|
Как подчистить контейнеры, если что-то пошло не так:
|
||||||
|
```
|
||||||
|
(cd dao && docker-compose down)
|
||||||
|
```
|
9
dao/dao.go
Normal file
9
dao/dao.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !solution
|
||||||
|
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func CreateDao(ctx context.Context, dsn string) (Dao, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
55
dao/dao_test.go
Normal file
55
dao/dao_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/slon/shad-go/pgfixture"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDao(t *testing.T) {
|
||||||
|
dsn := pgfixture.Start(t)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
dao, err := CreateDao(ctx, dsn)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = dao.Lookup(ctx, 42)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
aliceID, err := dao.Create(ctx, &User{Name: "Alice"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
bobID, err := dao.Create(ctx, &User{Name: "Bob"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
charlieID, err := dao.Create(ctx, &User{Name: "Charie"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, map[UserID]struct{}{aliceID: {}, bobID: {}, charlieID: {}}, 3)
|
||||||
|
|
||||||
|
alice, err := dao.Lookup(ctx, aliceID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, alice, User{ID: aliceID, Name: "Alice"})
|
||||||
|
|
||||||
|
require.NoError(t, dao.Delete(ctx, bobID))
|
||||||
|
|
||||||
|
_, err = dao.Lookup(ctx, bobID)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
require.NoError(t, dao.Update(ctx, &User{ID: charlieID, Name: "Chaplin"}))
|
||||||
|
|
||||||
|
users, err := dao.List(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sort.Slice(users, func(i, j int) bool {
|
||||||
|
return users[i].Name < users[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, users, []User{
|
||||||
|
{ID: aliceID, Name: "Alice"},
|
||||||
|
{ID: charlieID, Name: "Chaplin"},
|
||||||
|
})
|
||||||
|
}
|
10
dao/docker-compose.yaml
Normal file
10
dao/docker-compose.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
pg:
|
||||||
|
image: postgres:latest
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: shad-go
|
||||||
|
POSTGRES_USER: gopher
|
||||||
|
POSTGRES_PASSWORD: pass
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
22
dao/model.go
Normal file
22
dao/model.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
UserID int
|
||||||
|
|
||||||
|
User struct {
|
||||||
|
ID UserID
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
Dao interface {
|
||||||
|
Create(ctx context.Context, u *User) (UserID, error)
|
||||||
|
Update(ctx context.Context, u *User) error
|
||||||
|
Delete(ctx context.Context, id UserID) error
|
||||||
|
Lookup(ctx context.Context, id UserID) (User, error)
|
||||||
|
List(ctx context.Context) ([]User, error)
|
||||||
|
}
|
||||||
|
)
|
|
@ -94,7 +94,7 @@ go install ./digitalclock/...
|
||||||
3. форматирование времени: https://gobyexample.com/time-formatting-parsing
|
3. форматирование времени: https://gobyexample.com/time-formatting-parsing
|
||||||
4. работа с картинками: https://golang.org/pkg/image/
|
4. работа с картинками: https://golang.org/pkg/image/
|
||||||
|
|
||||||
Пример, создания простой png размера 5x5:
|
Пример создания простой png размера 5x5:
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,8 @@ type Job struct {
|
||||||
## Архитектура системы
|
## Архитектура системы
|
||||||
|
|
||||||
Наша система будет состоять из трех компонент.
|
Наша система будет состоять из трех компонент.
|
||||||
* Клиент - процесс запускающий сборку.
|
* Клиент - процесс, запускающий сборку.
|
||||||
* Воркер - процесс запускающий команды компиляции и тестирования.
|
* Воркер - процесс, запускающий команды компиляции и тестирования.
|
||||||
* Координатор - центральный процесс в системе, общается с клиентами и воркерами. Раздаёт задачи
|
* Координатор - центральный процесс в системе, общается с клиентами и воркерами. Раздаёт задачи
|
||||||
воркерам.
|
воркерам.
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ Client и Coordinator общаются через два вызова.
|
||||||
|
|
||||||
- `POST /build` - стартует новый билд.
|
- `POST /build` - стартует новый билд.
|
||||||
* Client посылает в Body запроса json c описанием сборки.
|
* Client посылает в Body запроса json c описанием сборки.
|
||||||
* Coordinator стримит в body ответа json сообщения описывающие прогресс сборки.
|
* Coordinator стримит в body ответа json сообщения, описывающие прогресс сборки.
|
||||||
* Первым сообщением в ответе Coordinator присылает `buildID`.
|
* Первым сообщением в ответе Coordinator присылает `buildID`.
|
||||||
* _Тут можно было бы использовать websocket, но нас устраивает более простое решение._
|
* _Тут можно было бы использовать websocket, но нас устраивает более простое решение._
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
|
|
||||||
`commit` помещает артефакт в кеш. `abort` отменяет запись артефакта, удаляя все данные.
|
`commit` помещает артефакт в кеш. `abort` отменяет запись артефакта, удаляя все данные.
|
||||||
|
|
||||||
После первого вызова `commit`, все последующие вызовы `commit` и `abort` должны ничего не делать.
|
После первого вызова `commit` все последующие вызовы `commit` и `abort` должны ничего не делать.
|
||||||
|
|
||||||
Точно так же, после вызова `abort`, все последующие вызовы `commit` и `abort` должны ничего не делать.
|
Точно так же, после вызова `abort` все последующие вызовы `commit` и `abort` должны ничего не делать.
|
||||||
|
|
||||||
Горутина может начать читать артефакт, позвав метод `Get`. Много горутин могут читать артефакт одновременно.
|
Горутина может начать читать артефакт, позвав метод `Get`. Много горутин могут читать артефакт одновременно.
|
||||||
Горутина должна позвать `unlock`, после того как она закончила работать с артефактом.
|
Горутина должна позвать `unlock`, после того как она закончила работать с артефактом.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package artifact
|
package artifact
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package artifact
|
package artifact
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package artifact
|
package artifact
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# build
|
# build
|
||||||
|
|
||||||
Пакет `build` содержит описание графа сборки и набор хелпер-функций для работы с графом. Вам не нужно
|
Пакет `build` содержит описание графа сборки и набор хелпер-функций для работы с графом. Вам не нужно
|
||||||
писать новый код в этом пакете, но нужно научиться пользоваться тем кодом который вам дан.
|
писать новый код в этом пакете, но нужно научиться пользоваться тем кодом, который вам дан.
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
# client
|
# client
|
||||||
|
|
||||||
Пакет `client` реализует клиента системы распределённой сборки. Клиент запускается локально, и имеет доступ к
|
Пакет `client` реализует клиента системы распределённой сборки. Клиент запускается локально и имеет доступ к
|
||||||
директории с исходным кодом.
|
директории с исходным кодом.
|
||||||
|
|
||||||
Клиент получает на вход `build.Graph` и запускает сборку на координаторе.
|
Клиент получает на вход `build.Graph` и запускает сборку на координаторе.
|
||||||
|
|
||||||
После того, как координатор создал новую сборку, клиент заливает недостающие файлы и посылает сигнал о завершении стадии заливки.
|
После того, как координатор создал новую сборку, клиент заливает недостающие файлы и посылает сигнал о завершении стадии заливки.
|
||||||
|
|
||||||
После этого, клиент следит за прогрессом сборки, дожидается завершения и выходит.
|
После этого клиент следит за прогрессом сборки, дожидается завершения и выходит.
|
||||||
|
|
||||||
Клиент тестируется интеграционными тестами из пакета `disttest`.
|
Клиент тестируется интеграционными тестами из пакета `disttest`.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
|
|
1
distbuild/pkg/dist/coordinator.go
vendored
1
distbuild/pkg/dist/coordinator.go
vendored
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package dist
|
package dist
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
- Вызов `GET /file?id=123` должен возвращать содержимое файла с `id=123`.
|
- Вызов `GET /file?id=123` должен возвращать содержимое файла с `id=123`.
|
||||||
- Вызов `PUT /file?id=123` должен заливать содержимое файла с `id=123`.
|
- Вызов `PUT /file?id=123` должен заливать содержимое файла с `id=123`.
|
||||||
|
|
||||||
**Обратите внимание:** Несколько клиентов могут начать заливать в кеш один и тот же набор файлов. В наивной реализации,
|
**Обратите внимание:** Несколько клиентов могут начать заливать в кеш один и тот же набор файлов. В наивной реализации
|
||||||
первый клиент залочит файл на запись, а следующие упадут с ошибкой. Ваш код должен обрабатывать эту ситуацию корректно,
|
первый клиент залочит файл на запись, а следующие упадут с ошибкой. Ваш код должен обрабатывать эту ситуацию корректно,
|
||||||
то есть последующие запросы должны дожидаться, пока первый запрос завершится. Для реализации этой логики
|
то есть последующие запросы должны дожидаться, пока первый запрос завершится. Для реализации этой логики
|
||||||
поведения вам поможет пакет [singleflight](https://godoc.org/golang.org/x/sync/singleflight).
|
поведения вам поможет пакет [singleflight](https://godoc.org/golang.org/x/sync/singleflight).
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package filecache
|
package filecache
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package filecache
|
package filecache
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package filecache
|
package filecache
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
воркерами.
|
воркерами.
|
||||||
|
|
||||||
Для того, чтобы зачесть домашнее задание, достаточно реализовать упрощённый алгоритм планирования с
|
Для того, чтобы зачесть домашнее задание, достаточно реализовать упрощённый алгоритм планирования с
|
||||||
одной глобальной очередью. Функция `ScheduleJob` должна помещать `job` в очередь, или возвращать ссылку на существующий
|
одной глобальной очередью. Функция `ScheduleJob` должна помещать `job` в очередь или возвращать ссылку на существующий
|
||||||
`pendingJob`. Функция `PickJob` должна извлекать первый элемент из очереди. Обратите внимание, что функция `PickJob`
|
`pendingJob`. Функция `PickJob` должна извлекать первый элемент из очереди. Обратите внимание, что функция `PickJob`
|
||||||
принимает контекст. Поскольку это блокирующая операция, она должна поддерживать отмены. Если вы забудете
|
принимает контекст. Поскольку это блокирующая операция, она должна поддерживать отмены. Если вы забудете
|
||||||
реализовать отмену в этом месте, интеграционные тесты будут зависать.
|
реализовать отмену в этом месте, интеграционные тесты будут зависать.
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
1. Одна глобальная очередь.
|
1. Одна глобальная очередь.
|
||||||
2. По две локальные очереди на воркер.
|
2. По две локальные очереди на воркер.
|
||||||
|
|
||||||
При запросе нового джоба воркер выбирает случайную джобу из трех очередей - глобальной, и двух локальных относящихся
|
При запросе нового джоба воркер выбирает случайную джобу из трех очередей - глобальной и двух локальных, относящихся
|
||||||
к этому воркеру. Случайная очередь выбирается одним вызовом `select {}`.
|
к этому воркеру. Случайная очередь выбирается одним вызовом `select {}`.
|
||||||
|
|
||||||
Ожидающий исполнения джоб всегда находится в первой локальной очереди воркеров, на которых есть
|
Ожидающий исполнения джоб всегда находится в первой локальной очереди воркеров, на которых есть
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
из множества зависимостей этого джоба.
|
из множества зависимостей этого джоба.
|
||||||
|
|
||||||
Определения первой и второй локальной очереди не зависят от того, в каком порядке в шедулер пришли джобы
|
Определения первой и второй локальной очереди не зависят от того, в каком порядке в шедулер пришли джобы
|
||||||
и информация о кеше артефактов. То есть, если джоб уже находится в глобальной очереди, и в этот момент приходит
|
и информация о кеше артефактов. То есть, если джоб уже находится в глобальной очереди и в этот момент приходит
|
||||||
информация, что этот джоб находится в кеше на воркере `W0`, то джоб должен быть добавлен
|
информация, что этот джоб находится в кеше на воркере `W0`, то джоб должен быть добавлен
|
||||||
в первую локальную очередь `W0`.
|
в первую локальную очередь `W0`.
|
||||||
|
|
||||||
|
@ -51,5 +51,5 @@
|
||||||
|
|
||||||
Вместо реального времени, юниттесты шедулера используют библиотеку `clockwork`. Это накладывает ограничения
|
Вместо реального времени, юниттесты шедулера используют библиотеку `clockwork`. Это накладывает ограничения
|
||||||
на детали вашей реализации. Ожидание `CacheTimeout` и `DepTimeout` должно быть реализовано как `select {}` на
|
на детали вашей реализации. Ожидание `CacheTimeout` и `DepTimeout` должно быть реализовано как `select {}` на
|
||||||
канале, который вернула функция `TimeAfter`. Мы считаем что `CacheTimeout < DepTimeout`, и ожидание этих
|
канале, который вернула функция `TimeAfter`. Мы считаем, что `CacheTimeout < DepTimeout`, и ожидание этих
|
||||||
таймаутов происходит последовательно в одной горутине.
|
таймаутов происходит последовательно в одной горутине.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package scheduler
|
package scheduler
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# tarstream
|
# tarstream
|
||||||
|
|
||||||
Вам нужно уметь передавать директорию с артефактами между воркерами. Для этого, вам нужно
|
Вам нужно уметь передавать директорию с артефактами между воркерами. Для этого вам нужно
|
||||||
реализовать две операции:
|
реализовать две операции:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package tarstream
|
package tarstream
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package worker
|
package worker
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,6 @@
|
||||||
go test -v ./dockertest/... -count=1
|
go test -v ./dockertest/... -count=1
|
||||||
```
|
```
|
||||||
|
|
||||||
Только **после того, как тесты пройдут локально** можете запушить решение в систему.
|
|
||||||
|
|
||||||
### С чего начать?
|
### С чего начать?
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -64,7 +62,7 @@ docker-compose up
|
||||||
|
|
||||||
Поискать решение проблемы в интернете.
|
Поискать решение проблемы в интернете.
|
||||||
|
|
||||||
Если решение найдено, и проблема выглядит общей, сделать merge request с улучшением README.
|
Если решение найдено и проблема выглядит общей, сделать merge request с улучшением README.
|
||||||
|
|
||||||
Если интернет не помог, спросить в чате.
|
Если интернет не помог, спросить в чате.
|
||||||
|
|
||||||
|
@ -82,7 +80,7 @@ docker-compose down
|
||||||
|
|
||||||
### Docker cheat sheet
|
### Docker cheat sheet
|
||||||
|
|
||||||
Получить список образов
|
Получить список образов:
|
||||||
```
|
```
|
||||||
docker images
|
docker images
|
||||||
```
|
```
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
студентам, но будут запускаться в момент проверки решения в
|
студентам, но будут запускаться в момент проверки решения в
|
||||||
тестирующей системе.
|
тестирующей системе.
|
||||||
- При посылке решения, на сервер отправляются все файлы внутри пакета.
|
- При посылке решения, на сервер отправляются все файлы внутри пакета.
|
||||||
- При тестировании, используются изменённые файлы пакета, и
|
- При тестировании, используются изменённые файлы пакета и
|
||||||
оригинальные файлы тестов.
|
оригинальные файлы тестов.
|
||||||
- Файл пакета можно защитить от изменения, добавив `//go:build !change` в начало файла.
|
- Файл пакета можно защитить от изменения, добавив `//go:build !change` в начало файла.
|
||||||
В этом случае при тестировании посылки всегда будет использоваться оригинальная версия файла.
|
В этом случае при тестировании посылки всегда будет использоваться оригинальная версия файла.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# dupcall
|
# dupcall
|
||||||
|
|
||||||
В этом задании нужно реализовать свою вариацию на тему singleflight.
|
В этом задании нужно реализовать свою вариацию на тему singleflight. Использовать в реализации singleflight нельзя.
|
||||||
|
|
||||||
Объект `dupcall.Call` должен дедуплицировать вызовы дорогой функции, правильно обрабатывая отмену контекста.
|
Объект `dupcall.Call` должен дедуплицировать вызовы дорогой функции, правильно обрабатывая отмену контекста.
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package dupcall
|
package dupcall
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package externalsort
|
package externalsort
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package externalsort
|
package externalsort
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package fileleak
|
package fileleak
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
В этой задаче нужно написать примитивный файрвол.
|
В этой задаче нужно написать примитивный файрвол.
|
||||||
|
|
||||||
Файрвол - это прокси сервер, пропускающий через себя все запросы,
|
Файрвол - это прокси сервер, пропускающий через себя все запросы
|
||||||
и отвергающий некоторые из них по заданному набору правил.
|
и отвергающий некоторые из них по заданному набору правил.
|
||||||
|
|
||||||
Пример правил можно посмотреть в [example.yaml](./configs/example.yaml).
|
Пример правил можно посмотреть в [example.yaml](./configs/example.yaml).
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,27 @@
|
||||||
|
|
||||||
В этом задании нужно реализовать консольную утилиту для подсчёта статистик авторов git репозитория.
|
В этом задании нужно реализовать консольную утилиту для подсчёта статистик авторов git репозитория.
|
||||||
|
|
||||||
|
```
|
||||||
|
✗ gitfame --repository=. --extensions='.go,.md' --order-by=lines
|
||||||
|
Name Lines Commits Files
|
||||||
|
Joe Tsai 12154 92 49
|
||||||
|
colinnewell 130 1 1
|
||||||
|
Roger Peppe 59 1 2
|
||||||
|
A. Ishikawa 36 1 1
|
||||||
|
Tobias Klauser 33 1 2
|
||||||
|
178inaba 11 2 4
|
||||||
|
Kyle Lemons 11 1 1
|
||||||
|
Dmitri Shuralyov 8 1 2
|
||||||
|
ferhat elmas 7 1 4
|
||||||
|
Christian Muehlhaeuser 6 3 4
|
||||||
|
k.nakada 5 1 3
|
||||||
|
LMMilewski 5 1 2
|
||||||
|
Ernest Galbrun 3 1 1
|
||||||
|
Ross Light 2 1 1
|
||||||
|
Chris Morrow 1 1 1
|
||||||
|
Fiisio 1 1 1
|
||||||
|
```
|
||||||
|
|
||||||
### Статистики
|
### Статистики
|
||||||
|
|
||||||
* Количество строк
|
* Количество строк
|
||||||
|
@ -10,6 +31,15 @@
|
||||||
|
|
||||||
Все статистики считаются для состояния репозитория на момент конкретного коммита.
|
Все статистики считаются для состояния репозитория на момент конкретного коммита.
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
|
||||||
|
Утилита должна печатать результат в stdout.
|
||||||
|
При использовании невалидного значения флага или любой другой ошибке программа должна завершаться с ненулевым кодом возврата.
|
||||||
|
|
||||||
|
Расчёт может занимать довольно длительное время.
|
||||||
|
Хорошим качеством подобной утилиты является отображение прогресса (вынесенное за флаг).
|
||||||
|
Прогресс можно печатать в stderr в произвольном формате.
|
||||||
|
|
||||||
#### Расчёт
|
#### Расчёт
|
||||||
|
|
||||||
Каждой строке интересующего подмножества файлов репозитория сопоставляется последний коммит, модифицировавший эту строку.
|
Каждой строке интересующего подмножества файлов репозитория сопоставляется последний коммит, модифицировавший эту строку.
|
||||||
|
@ -30,7 +60,7 @@ f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 5) Задача считае
|
||||||
```
|
```
|
||||||
|
|
||||||
`git blame` с флагом `--porcelain` (см. `git blame --help`) возвращает информацию в машиночитаемом формате.
|
`git blame` с флагом `--porcelain` (см. `git blame --help`) возвращает информацию в машиночитаемом формате.
|
||||||
Кроме того, этот формат схлопывает соседние строки относящиеся к одному коммиту,
|
Кроме того, этот формат схлопывает соседние строки, относящиеся к одному коммиту,
|
||||||
что может сильно сократить размер результата. Поэтому использовать нужно его.
|
что может сильно сократить размер результата. Поэтому использовать нужно его.
|
||||||
|
|
||||||
Стоит помнить, что не все файлы из директории git проекта обязательно принадлежат git репозиторию.
|
Стоит помнить, что не все файлы из директории git проекта обязательно принадлежат git репозиторию.
|
||||||
|
@ -41,15 +71,6 @@ f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 5) Задача считае
|
||||||
В процессе работы скрипт не должен менять состояние репозитория ни в какой момент,
|
В процессе работы скрипт не должен менять состояние репозитория ни в какой момент,
|
||||||
поскольку с репозиторием могут параллельно работать.
|
поскольку с репозиторием могут параллельно работать.
|
||||||
|
|
||||||
### Интерфейс
|
|
||||||
|
|
||||||
Утилита должна печатать результат в stdout.
|
|
||||||
При использовании невалидного значения флага или любой другой ошибке программа должна завершаться с ненулевым кодом возврата.
|
|
||||||
|
|
||||||
Расчёт может занимать довольно длительное время.
|
|
||||||
Хорошим качеством подобной утилиты является отображение прогресса (вынесенное за флаг).
|
|
||||||
Прогресс можно печатать в stderr в произвольном формате.
|
|
||||||
|
|
||||||
### Флаги
|
### Флаги
|
||||||
|
|
||||||
Утилита должна поддерживать следующий набор флагов:
|
Утилита должна поддерживать следующий набор флагов:
|
||||||
|
@ -58,7 +79,7 @@ f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 5) Задача считае
|
||||||
|
|
||||||
**--revision** — указатель на коммит; HEAD по умолчанию
|
**--revision** — указатель на коммит; HEAD по умолчанию
|
||||||
|
|
||||||
**--order-by** — ключ сортировки результатов, один из `lines` (дефолт), `commits`, `files`.
|
**--order-by** — ключ сортировки результатов; один из `lines` (дефолт), `commits`, `files`.
|
||||||
|
|
||||||
По умолчанию результаты сортируются по убыванию ключа `(lines, commits, files)`.
|
По умолчанию результаты сортируются по убыванию ключа `(lines, commits, files)`.
|
||||||
При равенстве ключей выше будет автор с лексикографически меньшим именем.
|
При равенстве ключей выше будет автор с лексикографически меньшим именем.
|
||||||
|
@ -116,7 +137,7 @@ ferhat elmas,1,1,1
|
||||||
|
|
||||||
### Тесты
|
### Тесты
|
||||||
|
|
||||||
Команда для запуска тестов
|
Команда для запуска тестов:
|
||||||
```
|
```
|
||||||
go test -v ./gitfame/test/integration/...
|
go test -v ./gitfame/test/integration/...
|
||||||
```
|
```
|
||||||
|
@ -124,7 +145,7 @@ go test -v ./gitfame/test/integration/...
|
||||||
В [/tests/integration/testdata/bundles](test/integration/testdata/bundles) лежат запакованные git репозитории.
|
В [/tests/integration/testdata/bundles](test/integration/testdata/bundles) лежат запакованные git репозитории.
|
||||||
Каждый интеграционный тест ссылается на какой-нибудь бандл.
|
Каждый интеграционный тест ссылается на какой-нибудь бандл.
|
||||||
|
|
||||||
Как создать bundle? Находясь в git репозитории выполнить
|
Как создать свой bundle? Находясь в git репозитории выполнить
|
||||||
```
|
```
|
||||||
git bundle create my.bundle --all
|
git bundle create my.bundle --all
|
||||||
```
|
```
|
||||||
|
@ -164,6 +185,7 @@ export PATH=$GOPATH/bin:$PATH
|
||||||
|
|
||||||
В небольших проектах нет ничего плохого в том, чтобы весь код лежал плоско в корне.
|
В небольших проектах нет ничего плохого в том, чтобы весь код лежал плоско в корне.
|
||||||
Здесь же, для ознакомления предлагаем изучить общепринятый подход.
|
Здесь же, для ознакомления предлагаем изучить общепринятый подход.
|
||||||
|
В частности, писать реализацию в internal или pkg (в чём разница?).
|
||||||
|
|
||||||
#### Cli
|
#### Cli
|
||||||
|
|
||||||
|
@ -174,18 +196,7 @@ export PATH=$GOPATH/bin:$PATH
|
||||||
В cobra используется библиотека [pflag](https://pkg.go.dev/github.com/spf13/pflag) для работы с флагами.
|
В cobra используется библиотека [pflag](https://pkg.go.dev/github.com/spf13/pflag) для работы с флагами.
|
||||||
Библиотеку можно использовать и отдельно от cobra.
|
Библиотеку можно использовать и отдельно от cobra.
|
||||||
`pflag` может побольше, чем стандартный [flag](https://golang.org/pkg/flag/),
|
`pflag` может побольше, чем стандартный [flag](https://golang.org/pkg/flag/),
|
||||||
в частости, в `pflag` есть полезные для решаемой задачи флаги для работы с аргументами-массивами.
|
в частности, в `pflag` есть полезные для решаемой задачи флаги для работы с аргументами-массивами.
|
||||||
|
|
||||||
Помимо библиотеки, в cobra есть ещё и бинарь (с именем cobra) для кодогенерации основы проекта.
|
|
||||||
|
|
||||||
Его можно установить в `GOPATH` командой
|
|
||||||
```
|
|
||||||
go get -u github.com/spf13/cobra
|
|
||||||
```
|
|
||||||
|
|
||||||
Генерируемый sample проект использует [viper](https://pkg.go.dev/github.com/spf13/viper) (библиотеку для работы с конфигами) и
|
|
||||||
[go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) (кроссплатформенную библиотеку для поиска домашней директории пользователя).
|
|
||||||
В этой задаче эти библиотеки не нужны, поэтому их нет в зависимостях проекта.
|
|
||||||
|
|
||||||
### Git ликбез
|
### Git ликбез
|
||||||
|
|
||||||
|
@ -207,7 +218,7 @@ go get -u github.com/spf13/cobra
|
||||||
**branch** (ветка) — это не объект `Git Object Database`, а всего лишь файл в директории `.git/refs/heads/` с хэшом последнего для этой ветки коммита.
|
**branch** (ветка) — это не объект `Git Object Database`, а всего лишь файл в директории `.git/refs/heads/` с хэшом последнего для этой ветки коммита.
|
||||||
То есть ветка — это указатель на коммит.
|
То есть ветка — это указатель на коммит.
|
||||||
|
|
||||||
**head** — это ссылка на коммит. В каждом репозитории по умолчанию есть **head** именем **master**.
|
**head** — это ссылка на коммит. В каждом репозитории по умолчанию есть **head** с именем **master**.
|
||||||
|
|
||||||
**HEAD** — один выделенный **head**. Файл `.git/HEAD`. Родитель следующего коммита.
|
**HEAD** — один выделенный **head**. Файл `.git/HEAD`. Родитель следующего коммита.
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package hogwarts
|
package hogwarts
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package hotelbusiness
|
package hotelbusiness
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package illegal
|
package illegal
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package illegal
|
package illegal
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
Обычные функции `json.Marshal` и `json.Unmarshal` работают с одним `json` объектом.
|
Обычные функции `json.Marshal` и `json.Unmarshal` работают с одним `json` объектом.
|
||||||
|
|
||||||
Иногда возникает ситуация, что вместо одного объекта, вам нужно передать последовательность объектов
|
Иногда возникает ситуация, что вместо одного объекта, вам нужно передать последовательность объектов,
|
||||||
разделённых пробельными символами. Например: `{"A": 1} {"B": 2} {"C": 3}`. Такую последовательность
|
разделённых пробельными символами. Например: `{"A": 1} {"B": 2} {"C": 3}`. Такую последовательность
|
||||||
можно прочитать используя `json.Decoder`, и можно записать используя `json.Encoder`.
|
можно прочитать используя `json.Decoder`, и можно записать используя `json.Encoder`.
|
||||||
|
|
||||||
Реализуйте функции `Marshal` и `Unmarshal`, которые работают со слайсом значений, и
|
Реализуйте функции `Marshal` и `Unmarshal`, которые работают со слайсом значений и
|
||||||
совершают подобное преобразование. По аналогии с пакетом `json`, функция `Marshal` принимает
|
совершают подобное преобразование. По аналогии с пакетом `json`, функция `Marshal` принимает
|
||||||
вторым аргументом слайс, а функция `Unmarshal` - указатель на слайс.
|
вторым аргументом слайс, а функция `Unmarshal` - указатель на слайс.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package jsonlist
|
package jsonlist
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package jsonrpc
|
package jsonrpc
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package keylock
|
package keylock
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ func BenchmarkKeyLock_NoBusyWait(b *testing.B) {
|
||||||
|
|
||||||
cancel := make(chan struct{})
|
cancel := make(chan struct{})
|
||||||
defer close(cancel)
|
defer close(cancel)
|
||||||
for i := 0; i < 10000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
l.LockKeys(lockedKey, cancel)
|
l.LockKeys(lockedKey, cancel)
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -242,10 +242,10 @@
|
||||||
|
|
||||||
Кодировка utf8
|
Кодировка utf8
|
||||||
|
|
||||||
0xxxxxx runes 0−127
|
0xxxxxxx runes 0−127
|
||||||
11xxxxx 10xxxxxx 128−2047
|
110xxxxx 10xxxxxx 128−2047
|
||||||
110xxxx 10xxxxxx 10xxxxxx 2048−65535
|
1110xxxx 10xxxxxx 10xxxxxx 2048−65535
|
||||||
1110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536−0x10ffff
|
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536−0x10ffff
|
||||||
|
|
||||||
Разница между рунами и байтами
|
Разница между рунами и байтами
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ Concurrency with Shared Memory
|
||||||
a := 1
|
a := 1
|
||||||
a += 1
|
a += 1
|
||||||
|
|
||||||
- Посылка значения из канал _happens_before_ получения этого значения из канала.
|
- Посылка значения из канала _happens_before_ получения этого значения из канала.
|
||||||
|
|
||||||
- Два события A и B _происходят_одновременно_ (_are_concurrent_), если нельзя сказать что одно случилось раньше другого.
|
- Два события A и B _происходят_одновременно_ (_are_concurrent_), если нельзя сказать что одно случилось раньше другого.
|
||||||
|
|
||||||
|
@ -312,18 +312,19 @@ Concurrency with Shared Memory
|
||||||
func (once *Once) Do(f func()) {
|
func (once *Once) Do(f func()) {
|
||||||
once.mu.Lock()
|
once.mu.Lock()
|
||||||
defer once.mu.Unlock()
|
defer once.mu.Unlock()
|
||||||
if done {
|
if once.done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if running {
|
if once.running {
|
||||||
once.cond.Wait() // releases and acquires mutex
|
once.cond.Wait() // releases and acquires mutex
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
running = true
|
|
||||||
|
once.running = true
|
||||||
once.mu.Unlock()
|
once.mu.Unlock()
|
||||||
f()
|
f()
|
||||||
once.mu.Lock()
|
once.mu.Lock()
|
||||||
done = true
|
once.done = true
|
||||||
once.cond.Broadcast()
|
once.cond.Broadcast()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package hasql
|
package hasql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/jackc/pgx/v4/stdlib"
|
_ "github.com/jackc/pgx/v4/stdlib"
|
||||||
"golang.yandex/hasql"
|
"golang.yandex/hasql"
|
||||||
|
@ -24,7 +27,7 @@ func Open() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Node address", node.Addr)
|
log.Println("Node address", node.Addr())
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Results(ctx context.Context, db *sql.DB) {
|
func Results(ctx context.Context, db *sql.DB) {
|
||||||
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE id = $1", 1)
|
rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE id = $1", 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package redis
|
package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
19
ledger/README.md
Normal file
19
ledger/README.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# ledger
|
||||||
|
|
||||||
|
Реализуйте объект для хранения банковских счетов. Требуемый интерфейс находится в `model.go`.
|
||||||
|
|
||||||
|
- Функция `New` должна создавать таблицу в базе данных.
|
||||||
|
- Метод `CreateAccount` должен создавать новый счёт с заданным `id`.
|
||||||
|
- Метод `GetBalance` должен возвращать текущий баланс.
|
||||||
|
- Метод `Deposit` должен зачислять деньги на счёт.
|
||||||
|
- Метод `Withdraw` должен снимать деньги со счёта.
|
||||||
|
Если на счету недостаточно денег, метод должен возвращать ошибку `ledger.ErrNoMoney`.
|
||||||
|
- Метод `Transfer` должен переводить деньги со счёта `from` на счёт `to`.
|
||||||
|
Если на счету `from` недостаточно денег, метод должен возвращать ошибку `ledger.ErrNoMoney`.
|
||||||
|
|
||||||
|
Все операции должны быть атомарными. Для реализации некоторых методов
|
||||||
|
вам потребуется использовать транзакции и row-level локи. Ваша реализация не должна создавать дедлоки на уровне базы данных.
|
||||||
|
|
||||||
|
Мы рекомендуем использовать функциональность `SELECT FOR UPDATE`.
|
||||||
|
|
||||||
|
Комментарии по запуску postgres смотрите в задаче [dao](../dao/).
|
10
ledger/docker-compose.yaml
Normal file
10
ledger/docker-compose.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
pg:
|
||||||
|
image: postgres:latest
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: shad-go
|
||||||
|
POSTGRES_USER: gopher
|
||||||
|
POSTGRES_PASSWORD: pass
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
9
ledger/ledger.go
Normal file
9
ledger/ledger.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !solution
|
||||||
|
|
||||||
|
package ledger
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func New(ctx context.Context, dsn string) (Ledger, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
135
ledger/ledger_test.go
Normal file
135
ledger/ledger_test.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package ledger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/slon/shad-go/ledger"
|
||||||
|
"gitlab.com/slon/shad-go/pgfixture"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLedger(t *testing.T) {
|
||||||
|
dsn := pgfixture.Start(t)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
l0, err := ledger.New(ctx, dsn)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("SimpleCommands", func(t *testing.T) {
|
||||||
|
checkBalance := func(account ledger.ID, amount ledger.Money) {
|
||||||
|
b, err := l0.GetBalance(ctx, account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, amount, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, l0.CreateAccount(ctx, "a0"))
|
||||||
|
checkBalance("a0", 0)
|
||||||
|
|
||||||
|
require.Error(t, l0.CreateAccount(ctx, "a0"))
|
||||||
|
|
||||||
|
require.NoError(t, l0.Deposit(ctx, "a0", ledger.Money(100)))
|
||||||
|
checkBalance("a0", 100)
|
||||||
|
|
||||||
|
require.NoError(t, l0.Withdraw(ctx, "a0", ledger.Money(50)))
|
||||||
|
checkBalance("a0", 50)
|
||||||
|
|
||||||
|
require.ErrorIs(t, l0.Withdraw(ctx, "a0", ledger.Money(100)), ledger.ErrNoMoney)
|
||||||
|
|
||||||
|
require.NoError(t, l0.CreateAccount(ctx, "a1"))
|
||||||
|
|
||||||
|
require.NoError(t, l0.Transfer(ctx, "a0", "a1", ledger.Money(40)))
|
||||||
|
checkBalance("a0", 10)
|
||||||
|
checkBalance("a1", 40)
|
||||||
|
|
||||||
|
require.ErrorIs(t, l0.Transfer(ctx, "a0", "a1", ledger.Money(50)), ledger.ErrNoMoney)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Transactions", func(t *testing.T) {
|
||||||
|
const nAccounts = 10
|
||||||
|
const initialBalance = 5
|
||||||
|
|
||||||
|
var accounts []ledger.ID
|
||||||
|
for i := 0; i < nAccounts; i++ {
|
||||||
|
id := ledger.ID(fmt.Sprint(i))
|
||||||
|
accounts = append(accounts, id)
|
||||||
|
|
||||||
|
require.NoError(t, l0.CreateAccount(ctx, id))
|
||||||
|
require.NoError(t, l0.Deposit(ctx, id, initialBalance))
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
spawn := func(action func() error) {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
if err := action(); err != nil {
|
||||||
|
if !errors.Is(err, ledger.ErrNoMoney) {
|
||||||
|
t.Errorf("operation failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < nAccounts; i++ {
|
||||||
|
i := i
|
||||||
|
|
||||||
|
account := accounts[i]
|
||||||
|
next := accounts[(i+1)%len(accounts)]
|
||||||
|
prev := accounts[(i+len(accounts)-1)%len(accounts)]
|
||||||
|
|
||||||
|
spawn(func() error {
|
||||||
|
balance, err := l0.GetBalance(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if balance < 0 {
|
||||||
|
return fmt.Errorf("%q balance is negative", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
spawn(func() error {
|
||||||
|
return l0.Transfer(ctx, account, next, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
spawn(func() error {
|
||||||
|
return l0.Transfer(ctx, account, prev, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
close(done)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
var total ledger.Money
|
||||||
|
for i := 0; i < nAccounts; i++ {
|
||||||
|
amount, err := l0.GetBalance(ctx, accounts[i])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
total += amount
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, ledger.Money(initialBalance*nAccounts), total)
|
||||||
|
})
|
||||||
|
}
|
21
ledger/model.go
Normal file
21
ledger/model.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package ledger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ID string
|
||||||
|
Money int64
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNoMoney = errors.New("no money")
|
||||||
|
|
||||||
|
type Ledger interface {
|
||||||
|
CreateAccount(ctx context.Context, id ID) error
|
||||||
|
GetBalance(ctx context.Context, id ID) (Money, error)
|
||||||
|
Deposit(ctx context.Context, id ID, amount Money) error
|
||||||
|
Withdraw(ctx context.Context, id ID, amount Money) error
|
||||||
|
Transfer(ctx context.Context, from, to ID, amount Money) error
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !change
|
//go:build !change
|
||||||
// +build !change
|
|
||||||
|
|
||||||
package lrucache
|
package lrucache
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package lrucache
|
package lrucache
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
### Что нужно сделать?
|
### Что нужно сделать?
|
||||||
|
|
||||||
Нужно написать реализацию Once используя каналы.
|
Нужно написать реализацию Once, используя каналы.
|
||||||
|
|
||||||
Использование пакета [sync](https://golang.org/pkg/sync) в этой задаче запрещено!
|
Использование пакета [sync](https://golang.org/pkg/sync) в этой задаче запрещено!
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package once
|
package once
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !solution
|
//go:build !solution
|
||||||
// +build !solution
|
|
||||||
|
|
||||||
package otp
|
package otp
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ func TestReader(t *testing.T) {
|
||||||
r := NewReader(testCase.r, testCase.prng)
|
r := NewReader(testCase.r, testCase.prng)
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(r)
|
buf, err := ioutil.ReadAll(r)
|
||||||
require.Equal(t, testCase.err, err)
|
require.ErrorIs(t, err, testCase.err)
|
||||||
require.Equal(t, testCase.result, buf)
|
require.Equal(t, testCase.result, buf)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ func TestWriterError(t *testing.T) {
|
||||||
n, err := w.Write(plaintext)
|
n, err := w.Write(plaintext)
|
||||||
|
|
||||||
require.Equalf(t, plaintextBackup, plaintext, "Write must not modify the slice data, even temporarily.")
|
require.Equalf(t, plaintextBackup, plaintext, "Write must not modify the slice data, even temporarily.")
|
||||||
require.Equal(t, err, iotest.ErrTimeout)
|
require.ErrorIs(t, err, iotest.ErrTimeout)
|
||||||
require.Equal(t, 512, n)
|
require.Equal(t, 512, n)
|
||||||
require.Equal(t, out.buf.Bytes(), ciphertext[:512])
|
require.Equal(t, out.buf.Bytes(), ciphertext[:512])
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue