Merge branch '5-concurrent-url-fetch' into 'master'

Resolve "concurrent-url-fetch"

Closes #5

See merge request slon/shad-go-private!5
This commit is contained in:
verytable 2020-02-13 20:04:47 +00:00
commit 228472b942
3 changed files with 219 additions and 0 deletions

50
fetchall/README.md Normal file
View file

@ -0,0 +1,50 @@
## fetchall
В этой задаче нужно написать консольную утилиту,
которая принимает на вход произвольное количество http URL'ов и скачивает их содержимое **конкурентно**.
Программа не должна останавливаться на невалидном URL'e.
Текст ответов можно игнорировать.
Вместо этого можно залогировать прогресс в произвольном формате.
Пример:
```
fetchall/solution.go https://gopl.io golang.org http://golang.org
Get golang.org: unsupported protocol scheme ""
1.05s 11071 http://golang.org
2.18s 4154 https://gopl.io
2.18s elapsed
```
В примере логируются времена обработки индивидуальных запросов, размеры ответов и общее время работы программы.
Можно видеть, что общее время работы равно максимуму, а не сумме времён индивидуальных запросов.
### Проверка решения
Для запуска тестов нужно выполнить следующую команду:
```
go test -v ./fetchall/...
```
### Запуск программы
```
go run -v ./fetchall/main.go
```
### Компиляция
```
go install ./fetchall/...
```
После выполнения в `$GOPATH/bin` появится исполняемый файл с именем `fetchall`.
### Ссылки
1. Чтение аргументов командной строки: https://gobyexample.com/command-line-arguments
2. HTTP запрос: https://gobyexample.com/http-clients
3. Запуск горутин: https://gobyexample.com/goroutines
4. Ожидание завершения горутин: https://gobyexample.com/channels
4. Замер времени: https://golang.org/pkg/time/#Since

7
fetchall/main.go Normal file
View file

@ -0,0 +1,7 @@
// +build !solution
package main
func main() {
}

162
fetchall/main_test.go Normal file
View file

@ -0,0 +1,162 @@
// +build !change
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"sort"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"gitlab.com/slon/shad-go/tools/testtool"
)
const fetchallImportPath = "gitlab.com/slon/shad-go/fetchall"
var binCache testtool.BinCache
func TestMain(m *testing.M) {
os.Exit(func() int {
var teardown testtool.CloseFunc
binCache, teardown = testtool.NewBinCache()
defer teardown()
return m.Run()
}())
}
func TestFetchall_valid(t *testing.T) {
binary, err := binCache.GetBinary(fetchallImportPath)
require.Nil(t, err)
type endpoint string
for _, tc := range []struct {
name string
h http.HandlerFunc
queries []endpoint
}{
{
name: "404",
h: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "The requested URL was not found.", http.StatusNotFound)
},
queries: []endpoint{"/" + endpoint(testtool.RandomName())},
},
{
name: "200",
h: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("The requested URL was found.\n"))
},
queries: []endpoint{"/"},
},
{
name: "multiple-urls",
h: func(w http.ResponseWriter, r *http.Request) {
mux := http.NewServeMux()
mux.HandleFunc("/foo", func(w http.ResponseWriter, h *http.Request) {
_, _ = w.Write([]byte("foo"))
})
mux.HandleFunc("/bar", func(w http.ResponseWriter, h *http.Request) {
_, _ = w.Write([]byte("bar"))
})
mux.ServeHTTP(w, r)
},
queries: []endpoint{"/foo", "/bar"},
},
} {
t.Run(tc.name, func(t *testing.T) {
s := httptest.NewServer(tc.h)
defer s.Close()
urls := make([]string, len(tc.queries))
for i, q := range tc.queries {
urls[i] = s.URL + string(q)
}
cmd := exec.Command(binary, urls...)
cmd.Stdout = nil
cmd.Stderr = os.Stderr
require.Nil(t, cmd.Run())
})
}
}
func TestFetchall_malformed(t *testing.T) {
binary, err := binCache.GetBinary(fetchallImportPath)
require.Nil(t, err)
hit := int32(0)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&hit, 1)
_, _ = w.Write([]byte("success"))
}))
defer s.Close()
cmd := exec.Command(binary, "golang.org", s.URL, s.URL)
cmd.Stdout = nil
cmd.Stderr = os.Stderr
err = cmd.Run()
require.Nil(t, err)
require.True(t, atomic.LoadInt32(&hit) >= 2)
}
func TestFetchall_concurrency(t *testing.T) {
binary, err := binCache.GetBinary(fetchallImportPath)
require.Nil(t, err)
var mu sync.Mutex
var callOrder []time.Duration
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("duration")
require.NotEmpty(t, s)
d, err := time.ParseDuration(s)
require.Nil(t, err)
time.Sleep(d)
mu.Lock()
callOrder = append(callOrder, d)
mu.Unlock()
_, _ = fmt.Fprintln(w, "hello")
}))
defer s.Close()
makeURL := func(d time.Duration) string {
v := url.Values{}
v.Add("duration", d.String())
return fmt.Sprintf("%s?%s", s.URL, v.Encode())
}
fastURL := makeURL(time.Millisecond * 10)
slowURL := makeURL(time.Second)
cmd := exec.Command(binary, slowURL, fastURL)
cmd.Stdout = nil
cmd.Stderr = os.Stderr
require.Nil(t, cmd.Run())
mu.Lock()
defer mu.Unlock()
require.Len(t, callOrder, 2)
require.True(t, sort.SliceIsSorted(callOrder, func(i, j int) bool {
return callOrder[i] < callOrder[j]
}))
}