remove: concurrent downloader
This commit is contained in:
@ -1,46 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
when the default case exists, messages are not guaranteed
|
||||
*/
|
||||
func main() {
|
||||
ch := make(chan int)
|
||||
|
||||
// if this GR is fast enough
|
||||
// the main GR can miss the signals
|
||||
go func() {
|
||||
var i int
|
||||
|
||||
for {
|
||||
i++
|
||||
|
||||
select {
|
||||
case ch <- i:
|
||||
default:
|
||||
// message is lost
|
||||
// it doesn't matter whether the chan is buffered or not
|
||||
}
|
||||
|
||||
if i == 10000 {
|
||||
fmt.Println("gopher dies")
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var total int
|
||||
for i := range ch {
|
||||
// works slower — misses some of the signals
|
||||
time.Sleep(time.Nanosecond)
|
||||
total += i
|
||||
}
|
||||
|
||||
// should be: 50005000
|
||||
fmt.Println(total)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a, b := make(chan bool), make(chan bool)
|
||||
|
||||
go func() {
|
||||
for a != nil || b != nil {
|
||||
fmt.Println("loop starts")
|
||||
|
||||
select {
|
||||
case <-a:
|
||||
fmt.Println("recv: a")
|
||||
a = nil
|
||||
case <-b:
|
||||
b = nil
|
||||
fmt.Println("recv: b")
|
||||
}
|
||||
|
||||
fmt.Println("loop ends")
|
||||
}
|
||||
fmt.Println("gopher dies")
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
// a <- true
|
||||
close(a)
|
||||
time.Sleep(time.Second)
|
||||
//b <- true
|
||||
close(b)
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
// closed chan never blocks
|
||||
// nil chan always blocks
|
||||
// if in the loop chans not set to nil, the loop will loop forever
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package fetch
|
||||
|
||||
// Drain drains the progress updates and returns the latest progresses
|
||||
func Drain(updates <-chan Progress) map[string]Progress {
|
||||
latest := make(map[string]Progress)
|
||||
|
||||
// save the latest progress
|
||||
for p := range updates {
|
||||
latest[p.URL] = p
|
||||
}
|
||||
return latest
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPGet requests to a url with the specified timeout.
|
||||
// And it returns the response with an error.
|
||||
// The caller should drain and close the body or it will leak.
|
||||
func HTTPGet(url string, timeout time.Duration) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
req.Close = true
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &http.Client{Timeout: timeout}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// checkout for the bad urls
|
||||
if s := resp.StatusCode; s < 200 || s > 299 {
|
||||
return resp, fmt.Errorf("bad status: %d", s)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPTransfer uses a fetcher and a storager to fetch and store an url
|
||||
type HTTPTransfer struct {
|
||||
Progress
|
||||
r io.ReadCloser
|
||||
w io.WriteCloser
|
||||
|
||||
timeout time.Duration
|
||||
|
||||
done <-chan bool
|
||||
updates chan<- Progress
|
||||
}
|
||||
|
||||
// NewHTTPTransfer creates and returns a new transfer
|
||||
func NewHTTPTransfer(url string, timeout time.Duration) *HTTPTransfer {
|
||||
return &HTTPTransfer{
|
||||
Progress: Progress{URL: url},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the transfer progress
|
||||
// The transfer will send its updates to the update chan
|
||||
// and it will stop when receives a signal from the done chan
|
||||
//
|
||||
// All the t.signal calls are here. I believe this increases the visibility
|
||||
// of the Start function (what it does). If the signal calls were in the
|
||||
// other funcs of HTTPTransfer, it could easily lead to bugs.
|
||||
func (t *HTTPTransfer) Start(updates chan<- Progress, done <-chan bool) {
|
||||
defer t.cleanup()
|
||||
|
||||
t.done, t.updates = done, updates
|
||||
|
||||
t.w, t.Error = os.Create(path.Base(t.URL))
|
||||
if t.Error != nil {
|
||||
t.signal()
|
||||
return
|
||||
}
|
||||
|
||||
t.request()
|
||||
if !t.signal() {
|
||||
return
|
||||
}
|
||||
|
||||
// sniff the on-going transfer until the signal returns false
|
||||
sniff := sniffer(func(p []byte) bool {
|
||||
l := len(p)
|
||||
t.Current = l
|
||||
t.Downloaded += l
|
||||
|
||||
return t.signal()
|
||||
})
|
||||
|
||||
t.transfer(sniff)
|
||||
t.signal()
|
||||
}
|
||||
|
||||
func (t *HTTPTransfer) cleanup() {
|
||||
if t.r != nil {
|
||||
t.r.Close()
|
||||
}
|
||||
if t.w != nil {
|
||||
t.w.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPTransfer) request() {
|
||||
var resp *http.Response
|
||||
|
||||
resp, t.Error = HTTPGet(t.URL, t.timeout)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Total = int(resp.ContentLength) // TODO: int(int64)
|
||||
if t.Total <= 0 {
|
||||
t.Error = fmt.Errorf("unknown content length: %d", t.Total)
|
||||
}
|
||||
|
||||
t.r = resp.Body
|
||||
}
|
||||
|
||||
func (t *HTTPTransfer) transfer(sniff sniffer) {
|
||||
// initiate the transfer and monitor it
|
||||
_, t.Error = io.Copy(io.MultiWriter(t.w, sniff), t.r)
|
||||
|
||||
// if the err is from sniffer ignore it
|
||||
if _, ok := t.Error.(sniffer); ok {
|
||||
t.Error = nil
|
||||
}
|
||||
|
||||
// the next signal will say: "no new bytes received" and its done
|
||||
t.Current = 0
|
||||
t.Done = true
|
||||
}
|
||||
|
||||
// signal signals the listeners about the last transfer progress.
|
||||
// it returns false when the done signal is received or there was an error
|
||||
// in the transfer.
|
||||
func (t *HTTPTransfer) signal() bool {
|
||||
select {
|
||||
case t.updates <- t.Progress:
|
||||
case <-t.done:
|
||||
// shutting down signal received
|
||||
return false
|
||||
}
|
||||
|
||||
// check the error only after sending the last progress
|
||||
// if this check was above, the last update won't be sent
|
||||
if t.Error != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// go on your duties
|
||||
return true
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (t *HTTPTransfer) fakeStart() {
|
||||
jitter()
|
||||
t.fakeRequest()
|
||||
t.signal()
|
||||
|
||||
for t.Downloaded < 100 {
|
||||
jitter()
|
||||
t.fakeFetch()
|
||||
|
||||
if !t.signal() {
|
||||
// done signal received or there was an error
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.fakeFinish()
|
||||
t.signal()
|
||||
}
|
||||
|
||||
// request requests to the url and adjusts the total length.
|
||||
func (t *HTTPTransfer) fakeRequest() {
|
||||
t.Total = 100
|
||||
|
||||
if debug {
|
||||
fmt.Printf("[TRANSFER] started: %s\n", t.URL)
|
||||
}
|
||||
}
|
||||
|
||||
// fetch fetches a bit from the resource
|
||||
func (t *HTTPTransfer) fakeFetch() {
|
||||
// TODO: right now hanged goroutine may hang the download completion
|
||||
// needs timeout
|
||||
// if t.URL == "url1" {
|
||||
// select {}
|
||||
// }
|
||||
|
||||
// NOTE: burada sayacli io.Writer kullan
|
||||
if t.URL == "url1" && t.Downloaded > rand.Intn(50) {
|
||||
t.Error = errors.New("cekemedim netten")
|
||||
}
|
||||
|
||||
n := rand.Intn(20) + 1
|
||||
if nn := t.Downloaded + n; nn > 100 {
|
||||
n = 100 - t.Downloaded
|
||||
}
|
||||
t.Current = n
|
||||
t.Downloaded += n
|
||||
}
|
||||
|
||||
// finish signals the finish signal to the listeners
|
||||
func (t *HTTPTransfer) fakeFinish() {
|
||||
t.Current = 0
|
||||
t.Done = true
|
||||
|
||||
if debug {
|
||||
fmt.Printf("[TRANSFER] DONE: %s\n", t.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func jitter() {
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)+1))
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package fetch
|
||||
|
||||
// Progress contains data about the downloading progress
|
||||
type Progress struct {
|
||||
URL string
|
||||
|
||||
Total, Downloaded, Current int
|
||||
Done bool
|
||||
Error error
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const debug = true
|
||||
|
||||
// Session manages the downloading process
|
||||
type Session struct {
|
||||
done chan bool
|
||||
}
|
||||
|
||||
// Transfer sends `updates` and terminates when `done` closes
|
||||
type Transfer interface {
|
||||
Start(updates chan<- Progress, done <-chan bool)
|
||||
}
|
||||
|
||||
// NewSession creates a new downloading session
|
||||
func NewSession() *Session {
|
||||
return &Session{done: make(chan bool)}
|
||||
}
|
||||
|
||||
// Start starts the downloading process
|
||||
func (s *Session) Start(transfers ...Transfer) <-chan Progress {
|
||||
// a buffered chan may unblock transfers in case of a slow ui
|
||||
updates := make(chan Progress)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(transfers))
|
||||
|
||||
for _, t := range transfers {
|
||||
go func(t Transfer) {
|
||||
defer wg.Done()
|
||||
t.Start(updates, s.done)
|
||||
}(t)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait() // wait until all downloads complete
|
||||
close(updates) // let the watchers (ui) know that we're shutting down
|
||||
}()
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Shutdown stops the downloading process and sends a signal to all parties
|
||||
func (s *Session) Shutdown() {
|
||||
// let the transfers know we're shutting down
|
||||
// when this is done s.updates will be closed in the Start() routine above
|
||||
close(s.done)
|
||||
|
||||
if debug {
|
||||
fmt.Printf("[SESSION ] DONE\n")
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package fetch
|
||||
|
||||
// sniffer converts a function to a sniffer that can sniff from the
|
||||
// on-going io.Writer call such as request -> file.
|
||||
//
|
||||
// This is here just for simplifying the logic of HTTPTransfer.
|
||||
type sniffer func(p []byte) bool
|
||||
|
||||
// Write satistifes io.Writer interface to sniff from it.
|
||||
// It can be used through a io.MultiWriter.
|
||||
func (f sniffer) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
// if the sniffer returns false, terminate with a non-nil error.
|
||||
// it used to abrupt the sniffing process, such as abrupting the
|
||||
// io.MultiWriter.
|
||||
if !f(p) {
|
||||
err = f
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Error satisfies the Error interface. So the returned error from the
|
||||
// sniffer.Write func can return itself as an error. This is only used when
|
||||
// the sniff.Write wants to terminate. So that we can distinguish it from a
|
||||
// real error.
|
||||
func (f sniffer) Error() string { return "" }
|
@ -1,44 +0,0 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// TODO: let main func hand the "TransferFactory" to "Session"
|
||||
// instead of "StorageFactory"
|
||||
//
|
||||
// Because: "Session" duplicates almost everything for "Transfer"
|
||||
|
||||
// StorageFactory func allows to switch storage implementations
|
||||
// When called it may return a FileStore
|
||||
// The transfer will call its Write method
|
||||
//
|
||||
// Why a func rather than an interface?
|
||||
// Because: We don't have to store any state
|
||||
// Storage will return its own io.Writer and the state will be in it
|
||||
// However, before invoking the storage, we don't need any state
|
||||
// We let us the storage manage its own state, we don't care about it
|
||||
type StorageFactory func(url string) io.Writer
|
||||
|
||||
// FileStorage is the default storage mechanism for the downloader
|
||||
// It writes to files
|
||||
type FileStorage struct {
|
||||
url string
|
||||
saved int
|
||||
}
|
||||
|
||||
// FileStorageFactory creates and returns a new FileStorage
|
||||
func FileStorageFactory(url string) io.Writer {
|
||||
return &FileStorage{url: url}
|
||||
}
|
||||
|
||||
func (f *FileStorage) Write(p []byte) (int, error) {
|
||||
if debug {
|
||||
fmt.Println("[FILESTORAGE]", string(p), "for", f.url)
|
||||
}
|
||||
// TODO:
|
||||
// if not exists create it
|
||||
// if exists update it
|
||||
return 0, nil
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/inancgumus/screen"
|
||||
)
|
||||
|
||||
const refreshPeriod = time.Second / 10
|
||||
|
||||
// uiProgress is the default UI for the downloader
|
||||
type uiProgress struct {
|
||||
urls []string
|
||||
transfers map[string]Progress
|
||||
}
|
||||
|
||||
// UI listens for the progress updates from the updates chan
|
||||
// and it refreshes the ui
|
||||
func UI(updates <-chan Progress) {
|
||||
ui := &uiProgress{transfers: make(map[string]Progress)}
|
||||
|
||||
// NOTE: we didn't use time.After here directly
|
||||
// because doing so can create a lot of Timer chans unnecessarily
|
||||
// instead we're just waiting on the same timer value
|
||||
tick := time.After(refreshPeriod)
|
||||
for {
|
||||
select {
|
||||
case p, ok := <-updates:
|
||||
// if no more updates close the ui
|
||||
if !ok {
|
||||
ui.shutdown()
|
||||
return
|
||||
}
|
||||
ui.update(p)
|
||||
|
||||
case <-tick:
|
||||
// `case <-tick:` allows updating the ui independently
|
||||
// from the progress update signals. or the ui would hang
|
||||
// the updaters (transfers).
|
||||
ui.refresh()
|
||||
tick = time.After(refreshPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// shutdown refreshes the ui for the last time and closes it
|
||||
func (ui *uiProgress) shutdown() {
|
||||
ui.refresh()
|
||||
|
||||
if debug {
|
||||
fmt.Printf("[ UI ] DONE\n")
|
||||
}
|
||||
}
|
||||
|
||||
// update updates the progress data from the received message
|
||||
func (ui *uiProgress) update(p Progress) {
|
||||
if _, ok := ui.transfers[p.URL]; !ok {
|
||||
ui.urls = append(ui.urls, p.URL)
|
||||
}
|
||||
|
||||
// update the latest progress for the url
|
||||
ui.transfers[p.URL] = p
|
||||
}
|
||||
|
||||
// refresh refreshes the UI with the latest progress
|
||||
func (ui *uiProgress) refresh() {
|
||||
if !debug {
|
||||
screen.Clear()
|
||||
screen.MoveTopLeft()
|
||||
}
|
||||
|
||||
var total, downloaded int
|
||||
|
||||
for _, u := range ui.urls {
|
||||
p := ui.transfers[u]
|
||||
|
||||
msg := "Downloading"
|
||||
if p.Done && p.Error == nil {
|
||||
msg = "👍 Completed"
|
||||
}
|
||||
if p.Error != nil {
|
||||
msg = fmt.Sprintf("❌ %s", p.Error)
|
||||
}
|
||||
|
||||
fmt.Println(p.URL)
|
||||
fmt.Printf("\t%d/%d\n", p.Downloaded, p.Total)
|
||||
fmt.Printf("\t%s\n", msg)
|
||||
|
||||
total += p.Total
|
||||
downloaded += p.Downloaded
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s %d/%d\n", "TOTAL DOWNLOADED BYTES:", downloaded, total)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
dl "github.com/inancgumus/learngo/31-concurrency/xxx-concurrent-downloader/fetch"
|
||||
)
|
||||
|
||||
//
|
||||
// +--- transfer #1 (on done/error signals and quits)
|
||||
// |
|
||||
// UI <--+--- transfer #2
|
||||
// |
|
||||
// +--- transfer #N
|
||||
// ^
|
||||
// |
|
||||
// SESSION ------+ launches goroutines
|
||||
//
|
||||
|
||||
//
|
||||
// +--- transfer #1
|
||||
// |
|
||||
// UI <--+--- transfer #2
|
||||
// |
|
||||
// +--- transfer #N
|
||||
// ^
|
||||
// |
|
||||
// SESSION ------+ launches goroutines
|
||||
//
|
||||
|
||||
//
|
||||
// +--- transfer #1
|
||||
// |
|
||||
// UI <--+--- transfer #2
|
||||
// ^ |
|
||||
// | +--- transfer #N
|
||||
// | ^
|
||||
// | |
|
||||
// +--------- SESSION
|
||||
//
|
||||
// Session can close the transfers with session.Shutdown()
|
||||
// (through session.done chan)
|
||||
//
|
||||
// Session closes the watcher chan when the transfers end
|
||||
// Possible problem: If any of the transfers never quit, session will hang.
|
||||
// I think, I'm going to manage that using context.Context in the transfers.
|
||||
//
|
||||
|
||||
func main() {
|
||||
// ========================================================================
|
||||
|
||||
// to := time.Second * 5
|
||||
// res, err := dl.HTTPGet("https://jsonplaceholder.typicode.com/todos/1", to)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
// }
|
||||
// defer res.Body.Close()
|
||||
|
||||
// io.Copy(os.Stdout, res.Body)
|
||||
|
||||
// return
|
||||
|
||||
// ========================================================================
|
||||
|
||||
sess := dl.NewSession()
|
||||
|
||||
// simulate a manual shutdown
|
||||
// time.AfterFunc(time.Second, func() {
|
||||
// sess.Shutdown()
|
||||
// })
|
||||
|
||||
to := time.Second * 5
|
||||
transfers := []dl.Transfer{
|
||||
dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1020x670-160kb.jpg", to),
|
||||
dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1020x670-610kb.jpg", to),
|
||||
dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1020x670-80kb.jpg", to),
|
||||
dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1900x1250-1700kb.jpg", to),
|
||||
}
|
||||
|
||||
// transfers := []dl.Transfer{
|
||||
// dl.NewHTTPTransfer(ctx, "http://inanc.io/1"),
|
||||
// dl.NewHTTPTransfer(ctx, "http://inanc.io/2"),
|
||||
// dl.NewHTTPTransfer(ctx, "http://inanc.io/3"),
|
||||
// }
|
||||
|
||||
dl.UI(sess.Start(transfers...))
|
||||
|
||||
// results := dl.Drain(sess.Start(urls))
|
||||
// for _, r := range results {
|
||||
// fmt.Printf("%s [err: %v — %d/%d]\n",
|
||||
// r.URL, r.Error, r.Downloaded, r.Total)
|
||||
// }
|
||||
|
||||
// how to handle ctrl+c signals?
|
||||
// register a signal
|
||||
// let the downloader now that the operation halts
|
||||
// with Downloader.onCancel or "context.Context"
|
||||
|
||||
// run with: GOTRACEBACK=all go run -race main.go
|
||||
// time.Sleep(time.Second * 2)
|
||||
// panic("give me the stack trace")
|
||||
}
|
Reference in New Issue
Block a user