Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add customizable timeout settings for the HTTP Client #250

Merged
merged 1 commit into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion dependency_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,31 @@ import (
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/BurntSushi/toml"
"github.com/buildpacks/libcnb"
"github.com/heroku/color"

"github.com/paketo-buildpacks/libpak/bard"
"github.com/paketo-buildpacks/libpak/sherpa"
)

type HttpClientTimeouts struct {
DialerTimeout time.Duration
DialerKeepAlive time.Duration
TLSHandshakeTimeout time.Duration
ResponseHeaderTimeout time.Duration
ExpectContinueTimeout time.Duration
}

// DependencyCache allows a user to get an artifact either from a buildpack's cache, a previous download, or to download
// directly.
type DependencyCache struct {
Expand All @@ -52,6 +64,9 @@ type DependencyCache struct {

// Mappings optionally provides URIs mapping for BuildpackDependencies
Mappings map[string]string

// httpClientTimeouts contains the timeout values used by HTTP client
HttpClientTimeouts HttpClientTimeouts
}

// NewDependencyCache creates a new instance setting the default cache path (<BUILDPACK_PATH>/dependencies) and user
Expand All @@ -69,9 +84,56 @@ func NewDependencyCache(context libcnb.BuildContext) (DependencyCache, error) {
return DependencyCache{}, fmt.Errorf("unable to process dependency-mapping bindings\n%w", err)
}
cache.Mappings = mappings

clientTimeouts, err := customizeHttpClientTimeouts()
if err != nil {
return DependencyCache{}, fmt.Errorf("unable to read custom timeout settings\n%w", err)
}
cache.HttpClientTimeouts = *clientTimeouts

return cache, nil
}

func customizeHttpClientTimeouts() (*HttpClientTimeouts, error) {
rawStr := sherpa.GetEnvWithDefault("BP_DIALER_TIMEOUT", "6")
dialerTimeout, err := strconv.Atoi(rawStr)
if err != nil {
return nil, fmt.Errorf("unable to convert BP_DIALER_TIMEOUT=%s to integer\n%w", rawStr, err)
}

rawStr = sherpa.GetEnvWithDefault("BP_DIALER_KEEP_ALIVE", "60")
dialerKeepAlive, err := strconv.Atoi(rawStr)
if err != nil {
return nil, fmt.Errorf("unable to convert BP_DIALER_KEEP_ALIVE=%s to integer\n%w", rawStr, err)
}

rawStr = sherpa.GetEnvWithDefault("BP_TLS_HANDSHAKE_TIMEOUT", "5")
tlsHandshakeTimeout, err := strconv.Atoi(rawStr)
if err != nil {
return nil, fmt.Errorf("unable to convert BP_TLS_HANDSHAKE_TIMEOUT=%s to integer\n%w", rawStr, err)
}

rawStr = sherpa.GetEnvWithDefault("BP_RESPONSE_HEADER_TIMEOUT", "5")
responseHeaderTimeout, err := strconv.Atoi(rawStr)
if err != nil {
return nil, fmt.Errorf("unable to convert BP_RESPONSE_HEADER_TIMEOUT=%s to integer\n%w", rawStr, err)
}

rawStr = sherpa.GetEnvWithDefault("BP_EXPECT_CONTINUE_TIMEOUT", "1")
expectContinueTimeout, err := strconv.Atoi(rawStr)
if err != nil {
return nil, fmt.Errorf("unable to convert BP_EXPECT_CONTINUE_TIMEOUT=%s to integer\n%w", rawStr, err)
}

return &HttpClientTimeouts{
DialerTimeout: time.Duration(dialerTimeout) * time.Second,
DialerKeepAlive: time.Duration(dialerKeepAlive) * time.Second,
TLSHandshakeTimeout: time.Duration(tlsHandshakeTimeout) * time.Second,
ResponseHeaderTimeout: time.Duration(responseHeaderTimeout) * time.Second,
ExpectContinueTimeout: time.Duration(expectContinueTimeout) * time.Second,
}, nil
}

func mappingsFromBindings(bindings libcnb.Bindings) (map[string]string, error) {
mappings := map[string]string{}
for _, binding := range bindings {
Expand Down Expand Up @@ -239,7 +301,18 @@ func (d DependencyCache) downloadHttp(uri string, destination string, mods ...Re
}
}

client := http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}}
client := http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: d.HttpClientTimeouts.DialerTimeout,
KeepAlive: d.HttpClientTimeouts.DialerKeepAlive,
}).Dial,
TLSHandshakeTimeout: d.HttpClientTimeouts.TLSHandshakeTimeout,
ResponseHeaderTimeout: d.HttpClientTimeouts.ResponseHeaderTimeout,
ExpectContinueTimeout: d.HttpClientTimeouts.ExpectContinueTimeout,
Proxy: http.ProxyFromEnvironment,
},
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("unable to request %s\n%w", uri, err)
Expand Down
30 changes: 30 additions & 0 deletions dependency_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,36 @@ func testDependencyCache(t *testing.T, context spec.G, it spec.S) {
Expect(dependencyCache.Mappings).To(Equal(map[string]string{}))
})

it("uses default timeout values", func() {
dependencyCache, err := libpak.NewDependencyCache(ctx)
Expect(err).NotTo(HaveOccurred())
Expect(dependencyCache.HttpClientTimeouts.DialerTimeout).To(Equal(6 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.DialerKeepAlive).To(Equal(60 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.TLSHandshakeTimeout).To(Equal(5 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.ResponseHeaderTimeout).To(Equal(5 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.ExpectContinueTimeout).To(Equal(1 * time.Second))
})

context("custom timeout setttings", func() {
it.Before(func() {
t.Setenv("BP_DIALER_TIMEOUT", "7")
t.Setenv("BP_DIALER_KEEP_ALIVE", "50")
t.Setenv("BP_TLS_HANDSHAKE_TIMEOUT", "2")
t.Setenv("BP_RESPONSE_HEADER_TIMEOUT", "3")
t.Setenv("BP_EXPECT_CONTINUE_TIMEOUT", "2")
})

it("uses custom timeout values", func() {
dependencyCache, err := libpak.NewDependencyCache(ctx)
Expect(err).NotTo(HaveOccurred())
Expect(dependencyCache.HttpClientTimeouts.DialerTimeout).To(Equal(7 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.DialerKeepAlive).To(Equal(50 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.TLSHandshakeTimeout).To(Equal(2 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.ResponseHeaderTimeout).To(Equal(3 * time.Second))
Expect(dependencyCache.HttpClientTimeouts.ExpectContinueTimeout).To(Equal(2 * time.Second))
})
})

context("bindings with type dependencies exist", func() {
it.Before(func() {
ctx.Platform.Bindings = libcnb.Bindings{
Expand Down