Skip to content

Commit

Permalink
Add customizable timeout settings for the HTTP Client
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
  • Loading branch information
dmikusa committed Jun 5, 2023
1 parent 48cf3e3 commit 45f8e75
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 1 deletion.
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

0 comments on commit 45f8e75

Please sign in to comment.