2015-01-25 06:32:33 +03:00
|
|
|
// Copyright 2015 CoreOS, Inc.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
2014-10-18 02:36:22 +04:00
|
|
|
|
2014-05-22 22:37:19 +04:00
|
|
|
package pkg
|
2014-05-21 21:13:20 +04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
neturl "net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
HTTP_2xx = 2
|
|
|
|
HTTP_4xx = 4
|
|
|
|
)
|
|
|
|
|
2014-06-23 22:32:31 +04:00
|
|
|
type Err error
|
|
|
|
|
2014-06-27 01:58:32 +04:00
|
|
|
type ErrTimeout struct {
|
2014-06-23 22:32:31 +04:00
|
|
|
Err
|
|
|
|
}
|
|
|
|
|
2014-06-27 01:58:32 +04:00
|
|
|
type ErrNotFound struct {
|
2014-06-23 22:32:31 +04:00
|
|
|
Err
|
|
|
|
}
|
|
|
|
|
2014-06-27 01:58:32 +04:00
|
|
|
type ErrInvalid struct {
|
|
|
|
Err
|
|
|
|
}
|
|
|
|
|
|
|
|
type ErrServer struct {
|
|
|
|
Err
|
|
|
|
}
|
|
|
|
|
|
|
|
type ErrNetwork struct {
|
2014-06-23 22:32:31 +04:00
|
|
|
Err
|
|
|
|
}
|
|
|
|
|
2014-05-21 21:13:20 +04:00
|
|
|
type HttpClient struct {
|
2015-08-08 05:23:56 +03:00
|
|
|
// Initial backoff duration. Defaults to 50 milliseconds
|
|
|
|
InitialBackoff time.Duration
|
|
|
|
|
2014-05-21 21:13:20 +04:00
|
|
|
// Maximum exp backoff duration. Defaults to 5 seconds
|
|
|
|
MaxBackoff time.Duration
|
|
|
|
|
2014-05-22 22:37:19 +04:00
|
|
|
// Maximum number of connection retries. Defaults to 15
|
2014-05-21 21:13:20 +04:00
|
|
|
MaxRetries int
|
|
|
|
|
2014-05-22 22:37:19 +04:00
|
|
|
// Whether or not to skip TLS verification. Defaults to false
|
2014-05-21 21:13:20 +04:00
|
|
|
SkipTLS bool
|
2014-06-27 01:58:32 +04:00
|
|
|
|
|
|
|
client *http.Client
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
|
|
|
|
2014-07-31 00:56:36 +04:00
|
|
|
type Getter interface {
|
2014-07-31 01:23:10 +04:00
|
|
|
Get(string) ([]byte, error)
|
2014-07-31 00:56:36 +04:00
|
|
|
GetRetry(string) ([]byte, error)
|
|
|
|
}
|
|
|
|
|
2014-05-21 21:13:20 +04:00
|
|
|
func NewHttpClient() *HttpClient {
|
2014-06-27 01:58:32 +04:00
|
|
|
hc := &HttpClient{
|
2015-08-08 05:23:56 +03:00
|
|
|
InitialBackoff: 50 * time.Millisecond,
|
|
|
|
MaxBackoff: time.Second * 5,
|
|
|
|
MaxRetries: 15,
|
|
|
|
SkipTLS: false,
|
|
|
|
client: &http.Client{
|
2015-08-11 20:42:41 +03:00
|
|
|
Timeout: 10 * time.Second,
|
2014-06-27 01:58:32 +04:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return hc
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
|
|
|
|
2014-06-27 02:17:53 +04:00
|
|
|
func ExpBackoff(interval, max time.Duration) time.Duration {
|
2014-05-29 22:03:15 +04:00
|
|
|
interval = interval * 2
|
|
|
|
if interval > max {
|
|
|
|
interval = max
|
|
|
|
}
|
|
|
|
return interval
|
|
|
|
}
|
|
|
|
|
2014-06-27 01:58:32 +04:00
|
|
|
// GetRetry fetches a given URL with support for exponential backoff and maximum retries
|
|
|
|
func (h *HttpClient) GetRetry(rawurl string) ([]byte, error) {
|
2014-05-21 21:13:20 +04:00
|
|
|
if rawurl == "" {
|
2014-06-23 22:32:31 +04:00
|
|
|
return nil, ErrInvalid{errors.New("URL is empty. Skipping.")}
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
url, err := neturl.Parse(rawurl)
|
|
|
|
if err != nil {
|
2014-06-23 22:32:31 +04:00
|
|
|
return nil, ErrInvalid{err}
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Unfortunately, url.Parse is too generic to throw errors if a URL does not
|
|
|
|
// have a valid HTTP scheme. So, we have to do this extra validation
|
|
|
|
if !strings.HasPrefix(url.Scheme, "http") {
|
2014-06-23 22:32:31 +04:00
|
|
|
return nil, ErrInvalid{fmt.Errorf("URL %s does not have a valid HTTP scheme. Skipping.", rawurl)}
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
dataURL := url.String()
|
|
|
|
|
2015-08-08 05:23:56 +03:00
|
|
|
duration := h.InitialBackoff
|
2014-05-21 21:13:20 +04:00
|
|
|
for retry := 1; retry <= h.MaxRetries; retry++ {
|
|
|
|
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
|
|
|
|
|
2014-06-27 01:58:32 +04:00
|
|
|
data, err := h.Get(dataURL)
|
|
|
|
switch err.(type) {
|
|
|
|
case ErrNetwork:
|
|
|
|
log.Printf(err.Error())
|
|
|
|
case ErrServer:
|
|
|
|
log.Printf(err.Error())
|
|
|
|
case ErrNotFound:
|
|
|
|
return data, err
|
|
|
|
default:
|
|
|
|
return data, err
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
|
|
|
|
2014-06-27 02:17:53 +04:00
|
|
|
duration = ExpBackoff(duration, h.MaxBackoff)
|
2014-05-22 22:53:54 +04:00
|
|
|
log.Printf("Sleeping for %v...", duration)
|
2014-05-21 21:13:20 +04:00
|
|
|
time.Sleep(duration)
|
|
|
|
}
|
|
|
|
|
2014-06-23 22:32:31 +04:00
|
|
|
return nil, ErrTimeout{fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)}
|
2014-05-21 21:13:20 +04:00
|
|
|
}
|
2014-06-27 01:58:32 +04:00
|
|
|
|
|
|
|
func (h *HttpClient) Get(dataURL string) ([]byte, error) {
|
|
|
|
if resp, err := h.client.Get(dataURL); err == nil {
|
|
|
|
defer resp.Body.Close()
|
|
|
|
switch resp.StatusCode / 100 {
|
|
|
|
case HTTP_2xx:
|
|
|
|
return ioutil.ReadAll(resp.Body)
|
|
|
|
case HTTP_4xx:
|
|
|
|
return nil, ErrNotFound{fmt.Errorf("Not found. HTTP status code: %d", resp.StatusCode)}
|
|
|
|
default:
|
|
|
|
return nil, ErrServer{fmt.Errorf("Server error. HTTP status code: %d", resp.StatusCode)}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, ErrNetwork{fmt.Errorf("Unable to fetch data: %s", err.Error())}
|
|
|
|
}
|
|
|
|
}
|