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:
commit
228472b942
3 changed files with 219 additions and 0 deletions
50
fetchall/README.md
Normal file
50
fetchall/README.md
Normal 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
7
fetchall/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
}
|
162
fetchall/main_test.go
Normal file
162
fetchall/main_test.go
Normal 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]
|
||||||
|
}))
|
||||||
|
}
|
Loading…
Reference in a new issue