diff --git a/go/README.md b/go/README.md index c20af3156..144eb339f 100644 --- a/go/README.md +++ b/go/README.md @@ -118,3 +118,57 @@ func main() { } } ``` +### Custom Context + +If you want even greater control over the lifecycle of the HTTP requests consider providing a [context](https://pkg.go.dev/context). Contexts can be set for all requests or per request. They can be combined with the timeout options mentioned in the previous section to fine-tune the lifecycle of your requests. + +If you wish to include timeout functionality in your custom context then you should leverage [context.WithTimeout](https://pkg.go.dev/context#WithTimeout). + +> Note: Custom contexts will be used as the parent context for any timeout you set as specified in the previous section. If the parent context gets cancelled it will propagate to the child context, but if the timeout context times out it does not propagate to the parent context. + +#### Custom Context for all requests + +Follow the example code snippet below if you want all requests to use the same parent context: + +```go +import "context" + +func main() { + // sets a timeout of 5 minutes + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + cfg, err := rtl.NewSettingsFromFile("path/to/looker.ini", nil) + cfg.Context = ctx + + session := rtl.NewAuthSession(cfg) + sdk := v4.NewLookerSDK(session) +} +``` + +> Note: A context set here will become the parent context for all API calls as well as all requests to fetch/refresh oauth tokens, which are normally completely isolated from contexts set via the Timeout property. In this case the token refresh requests and each individual API call will share a common parent context. + +#### Custom Context per request + +Follow the example here to set a context for a specific request. + +> Note: This will be used as the parent context for any timeout setting you've specified for API calls. If you've set contexts in both your API config and in the request options the request options context will be used instead. Background requests to fetch/refresh oauth tokens will NOT use a context set via request options - it will default to use a generic background context or, if you've also set a context in the API config it will still use that as specified in the previous section. + +```go +import "context" + +func main() { + // sets a timeout of 5 minutes + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + cfg, err := rtl.NewSettingsFromFile("path/to/looker.ini", nil) + session := rtl.NewAuthSession(cfg) + sdk := v4.NewLookerSDK(session) + + sdk.Me("", &ApiSettings{Context: ctx}) +} +``` + +> Note: Setting a context per request will NOT affect the context used for the background token fetching requests. If you have also set a context for all requests as mentioned above then that context +will still be used for the token requests, otherwise the SDK will fall back on using a completely separate context for the token fetching requests. \ No newline at end of file diff --git a/go/rtl/auth.go b/go/rtl/auth.go index d993bec7f..855333ce7 100644 --- a/go/rtl/auth.go +++ b/go/rtl/auth.go @@ -67,8 +67,13 @@ func NewAuthSessionWithTransport(config ApiSettings, transport http.RoundTripper AuthStyle: oauth2.AuthStyleInParams, } + bgCtx := context.Background() + if config.Context != nil { + bgCtx = config.Context + } + ctx := context.WithValue( - context.Background(), + bgCtx, oauth2.HTTPClient, // Will set "x-looker-appid" Header on TokenURL requests &http.Client{Transport: appIdHeaderTransport}, @@ -123,8 +128,16 @@ func (s *AuthSession) Do(result interface{}, method, ver, path string, reqPars m } } - // create request context with timeout - var timeoutInSeconds int32 = 120 //seconds + parent := context.Background() + if s.Config.Context != nil { + parent = s.Config.Context + } + if options != nil && options.Context != nil { + parent = options.Context + } + + // create request context with timeout from options or else config or else 120 seconds + var timeoutInSeconds int32 = 120 if s.Config.Timeout != 0 { timeoutInSeconds = s.Config.Timeout } @@ -132,7 +145,7 @@ func (s *AuthSession) Do(result interface{}, method, ver, path string, reqPars m timeoutInSeconds = options.Timeout } - ctx, cncl := context.WithTimeout(context.Background(), time.Second*time.Duration(timeoutInSeconds)) + ctx, cncl := context.WithTimeout(parent, time.Second*time.Duration(timeoutInSeconds)) defer cncl() // create new request diff --git a/go/rtl/auth_test.go b/go/rtl/auth_test.go index c63754e52..d955cebe3 100644 --- a/go/rtl/auth_test.go +++ b/go/rtl/auth_test.go @@ -635,6 +635,164 @@ func TestAuthSession_Do_Timeout(t *testing.T) { t.Errorf("Do() call did not error with context.DeadlineExceeded, got=%v", err) } }) + + t.Run("Do() follows Context set in AuthSession config", func(t *testing.T) { + mux := http.NewServeMux() + setupApi40Login(mux, foreverValidTestToken, http.StatusOK) + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api"+apiVersion+path, func(w http.ResponseWriter, r *http.Request) { + time.Sleep(4 * time.Second) + }) + + ctx, cncl := context.WithTimeout(context.Background(), 1*time.Second) + defer cncl() + + session := NewAuthSession(ApiSettings{ + BaseUrl: server.URL, + ApiVersion: apiVersion, + Context: ctx, + }) + + err := session.Do(nil, "GET", apiVersion, path, nil, nil, nil) + + if err == nil { + t.Errorf("Do() call did not error/timeout") + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("Do() call did not error with context.DeadlineExceeded, got=%v", err) + } + }) + + t.Run("Do() follows Context set in Do()'s options", func(t *testing.T) { + mux := http.NewServeMux() + setupApi40Login(mux, foreverValidTestToken, http.StatusOK) + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api"+apiVersion+path, func(w http.ResponseWriter, r *http.Request) { + time.Sleep(4 * time.Second) + }) + + ctx, cncl := context.WithTimeout(context.Background(), 1*time.Second) + defer cncl() + + session := NewAuthSession(ApiSettings{ + BaseUrl: server.URL, + ApiVersion: apiVersion, + }) + + options := ApiSettings{ + Context: ctx, + } + + err := session.Do(nil, "GET", apiVersion, path, nil, nil, &options) + + if err == nil { + t.Errorf("Do() call did not error/timeout") + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("Do() call did not error with context.DeadlineExceeded, got=%v", err) + } + }) + + t.Run("Timeout set in Do()'s options overrides Authsession", func(t *testing.T) { + mux := http.NewServeMux() + setupApi40Login(mux, foreverValidTestToken, http.StatusOK) + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api"+apiVersion+path, func(w http.ResponseWriter, r *http.Request) { + time.Sleep(4 * time.Second) + }) + + session := NewAuthSession(ApiSettings{ + BaseUrl: server.URL, + ApiVersion: apiVersion, + Timeout: 5, + }) + + options := ApiSettings{ + Timeout: 1, //seconds + } + + err := session.Do(nil, "GET", apiVersion, path, nil, nil, &options) + + if err == nil { + t.Errorf("Do() call did not error/timeout") + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("Do() call did not error with context.DeadlineExceeded, got=%v", err) + } + }) + + t.Run("Parent context timeout propagates to timeout child context", func(t *testing.T) { + mux := http.NewServeMux() + setupApi40Login(mux, foreverValidTestToken, http.StatusOK) + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api"+apiVersion+path, func(w http.ResponseWriter, r *http.Request) { + time.Sleep(4 * time.Second) + }) + + ctx, cncl := context.WithTimeout(context.Background(), 1*time.Second) + defer cncl() + + session := NewAuthSession(ApiSettings{ + BaseUrl: server.URL, + ApiVersion: apiVersion, + Context: ctx, + Timeout: 5, + }) + + options := ApiSettings{ + Timeout: 5, + } + + err := session.Do(nil, "GET", apiVersion, path, nil, nil, &options) + + if err == nil { + t.Errorf("Do() call did not error/timeout") + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("Do() call did not error with context.DeadlineExceeded, got=%v", err) + } + }) + + t.Run("Parent context set in options overrides config ctx and propagates to child timout", func(t *testing.T) { + mux := http.NewServeMux() + setupApi40Login(mux, foreverValidTestToken, http.StatusOK) + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api"+apiVersion+path, func(w http.ResponseWriter, r *http.Request) { + time.Sleep(4 * time.Second) + }) + + octx, ocncl := context.WithTimeout(context.Background(), 1*time.Second) + defer ocncl() + + sctx, scncl := context.WithTimeout(context.Background(), 5*time.Second) + defer scncl() + + session := NewAuthSession(ApiSettings{ + BaseUrl: server.URL, + ApiVersion: apiVersion, + Context: sctx, + Timeout: 5, + }) + + options := ApiSettings{ + Timeout: 5, + Context: octx, + } + + err := session.Do(nil, "GET", apiVersion, path, nil, nil, &options) + + if err == nil { + t.Errorf("Do() call did not error/timeout") + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("Do() call did not error with context.DeadlineExceeded, got=%v", err) + } + }) } func TestSetQuery(t *testing.T) { diff --git a/go/rtl/settings.go b/go/rtl/settings.go index f9d1334fa..4cd04724e 100644 --- a/go/rtl/settings.go +++ b/go/rtl/settings.go @@ -1,11 +1,13 @@ package rtl import ( + "context" "fmt" - "gopkg.in/ini.v1" "os" "strconv" "strings" + + "gopkg.in/ini.v1" ) var defaultSectionName string = "Looker" @@ -20,6 +22,7 @@ type ApiSettings struct { ClientSecret string `ini:"client_secret"` ApiVersion string `ini:"api_version"` Headers map[string]string + Context context.Context } var defaultSettings ApiSettings = ApiSettings{