From 4d8c6a28a2a3c24e1fcc2d0bed8220b7eeffa1c0 Mon Sep 17 00:00:00 2001 From: Ben Buzbee Date: Mon, 7 Sep 2020 03:27:53 +0000 Subject: [PATCH 1/3] Add API support for cancelation context's passed via QueryOptions Copy Consul API's format: QueryOptions.WithContext(context) will now return a new QueryOption whose HTTP requests wiill be canceled with the context provided. --- api/api.go | 34 +++++++++++++++++++++++++++++++++- api/api_test.go | 20 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 54ede4f89c64..4dd5d9a4541e 100644 --- a/api/api.go +++ b/api/api.go @@ -3,6 +3,7 @@ package api import ( "bytes" "compress/gzip" + "context" "crypto/tls" "encoding/json" "errors" @@ -63,6 +64,10 @@ type QueryOptions struct { // AuthToken is the secret ID of an ACL token AuthToken string + + // ctx is an optional context pass through to the underlying HTTP + // request layer. Use Context() and WithContext() to manage this. + ctx context.Context } // WriteOptions are used to parametrize a write @@ -517,6 +522,7 @@ type request struct { token string body io.Reader obj interface{} + ctx context.Context } // setQueryOptions is used to annotate the request with @@ -549,6 +555,7 @@ func (r *request) setQueryOptions(q *QueryOptions) { for k, v := range q.Params { r.params.Set(k, v) } + r.ctx = q.Context() } // durToMsec converts a duration to a millisecond specified string @@ -587,8 +594,15 @@ func (r *request) toHTTP() (*http.Request, error) { } } + ctx := func() context.Context { + if r.ctx != nil { + return r.ctx + } + return context.Background() + }() + // Create the HTTP request - req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) + req, err := http.NewRequestWithContext(ctx, r.method, r.url.RequestURI(), r.body) if err != nil { return nil, err } @@ -982,3 +996,21 @@ func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *h } return d, resp, nil } + +// Context returns the context used for canceling HTTP requests related to this query +func (o *QueryOptions) Context() context.Context { + if o != nil && o.ctx != nil { + return o.ctx + } + return context.Background() +} + +// WithContext creates a copy of the query options using the provided context to cancel related HTTP requests +func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions { + o2 := new(QueryOptions) + if o != nil { + *o2 = *o + } + o2.ctx = ctx + return o2 +} diff --git a/api/api_test.go b/api/api_test.go index e15897da46ad..39a845221448 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,7 +1,9 @@ package api import ( + "context" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -198,6 +200,24 @@ func TestSetQueryOptions(t *testing.T) { } } +func TestContext(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + c, s := makeClient(t, nil, nil) + defer s.Stop() + q := (&QueryOptions{ + WaitIndex: 10000, + }).WithContext(ctx) + // check if eq + go func() { + cancel() + }() + _, _, err := c.Jobs().List(q) + if !errors.Is(err, context.Canceled) { + t.Fatalf("Expected job wait to fail with canceled, got %s", err) + } +} + func TestSetWriteOptions(t *testing.T) { t.Parallel() c, s := makeClient(t, nil, nil) From a185409ef197600ef85c395feb0d8cde351efd97 Mon Sep 17 00:00:00 2001 From: Ben Buzbee Date: Wed, 9 Sep 2020 16:42:49 +0000 Subject: [PATCH 2/3] Add similar context api to WriteOptions --- api/api.go | 23 +++++++++++++++++++++++ api/api_test.go | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/api/api.go b/api/api.go index 4dd5d9a4541e..e54d37bb983a 100644 --- a/api/api.go +++ b/api/api.go @@ -81,6 +81,10 @@ type WriteOptions struct { // AuthToken is the secret ID of an ACL token AuthToken string + + // ctx is an optional context pass through to the underlying HTTP + // request layer. Use Context() and WithContext() to manage this. + ctx context.Context } // QueryMeta is used to return meta data about a query @@ -578,6 +582,7 @@ func (r *request) setWriteOptions(q *WriteOptions) { if q.AuthToken != "" { r.token = q.AuthToken } + r.ctx = q.Context() } // toHTTP converts the request to an HTTP request @@ -1014,3 +1019,21 @@ func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions { o2.ctx = ctx return o2 } + +// Context returns the context used for canceling HTTP requests related to this write +func (o *WriteOptions) Context() context.Context { + if o != nil && o.ctx != nil { + return o.ctx + } + return context.Background() +} + +// WithContext creates a copy of the write options using the provided context to cancel related HTTP requests +func (o *WriteOptions) WithContext(ctx context.Context) *WriteOptions { + o2 := new(WriteOptions) + if o != nil { + *o2 = *o + } + o2.ctx = ctx + return o2 +} diff --git a/api/api_test.go b/api/api_test.go index 39a845221448..d460987963ad 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -200,7 +200,7 @@ func TestSetQueryOptions(t *testing.T) { } } -func TestContext(t *testing.T) { +func TestQueryOptionsContext(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) c, s := makeClient(t, nil, nil) @@ -208,13 +208,42 @@ func TestContext(t *testing.T) { q := (&QueryOptions{ WaitIndex: 10000, }).WithContext(ctx) - // check if eq + + if q.ctx != ctx { + t.Fatalf("expected context to be set") + } + go func() { cancel() }() _, _, err := c.Jobs().List(q) if !errors.Is(err, context.Canceled) { - t.Fatalf("Expected job wait to fail with canceled, got %s", err) + t.Fatalf("expected job wait to fail with canceled, got %s", err) + } +} + +func TestWriteOptionsContext(t *testing.T) { + // No blocking query to test a real cancel of a pending request so + // just test that if we pass a pre-canceled context, writes fail quickly + t.Parallel() + + c, err := NewClient(DefaultConfig()) + if err != nil { + t.Fatalf("failed to initialize client: %s", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + w := (&WriteOptions{}).WithContext(ctx) + + if w.ctx != ctx { + t.Fatalf("expected context to be set") + } + + cancel() + + _, _, err = c.Jobs().Deregister("jobid", true, w) + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected job to fail with canceled, got %s", err) } } From 47a7ce8604654f02624f708cc924642bfd9061ed Mon Sep 17 00:00:00 2001 From: Ben Buzbee Date: Wed, 9 Sep 2020 16:43:56 +0000 Subject: [PATCH 3/3] Run make sync --- vendor/github.com/hashicorp/nomad/api/api.go | 57 +++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/vendor/github.com/hashicorp/nomad/api/api.go b/vendor/github.com/hashicorp/nomad/api/api.go index 54ede4f89c64..e54d37bb983a 100644 --- a/vendor/github.com/hashicorp/nomad/api/api.go +++ b/vendor/github.com/hashicorp/nomad/api/api.go @@ -3,6 +3,7 @@ package api import ( "bytes" "compress/gzip" + "context" "crypto/tls" "encoding/json" "errors" @@ -63,6 +64,10 @@ type QueryOptions struct { // AuthToken is the secret ID of an ACL token AuthToken string + + // ctx is an optional context pass through to the underlying HTTP + // request layer. Use Context() and WithContext() to manage this. + ctx context.Context } // WriteOptions are used to parametrize a write @@ -76,6 +81,10 @@ type WriteOptions struct { // AuthToken is the secret ID of an ACL token AuthToken string + + // ctx is an optional context pass through to the underlying HTTP + // request layer. Use Context() and WithContext() to manage this. + ctx context.Context } // QueryMeta is used to return meta data about a query @@ -517,6 +526,7 @@ type request struct { token string body io.Reader obj interface{} + ctx context.Context } // setQueryOptions is used to annotate the request with @@ -549,6 +559,7 @@ func (r *request) setQueryOptions(q *QueryOptions) { for k, v := range q.Params { r.params.Set(k, v) } + r.ctx = q.Context() } // durToMsec converts a duration to a millisecond specified string @@ -571,6 +582,7 @@ func (r *request) setWriteOptions(q *WriteOptions) { if q.AuthToken != "" { r.token = q.AuthToken } + r.ctx = q.Context() } // toHTTP converts the request to an HTTP request @@ -587,8 +599,15 @@ func (r *request) toHTTP() (*http.Request, error) { } } + ctx := func() context.Context { + if r.ctx != nil { + return r.ctx + } + return context.Background() + }() + // Create the HTTP request - req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) + req, err := http.NewRequestWithContext(ctx, r.method, r.url.RequestURI(), r.body) if err != nil { return nil, err } @@ -982,3 +1001,39 @@ func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *h } return d, resp, nil } + +// Context returns the context used for canceling HTTP requests related to this query +func (o *QueryOptions) Context() context.Context { + if o != nil && o.ctx != nil { + return o.ctx + } + return context.Background() +} + +// WithContext creates a copy of the query options using the provided context to cancel related HTTP requests +func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions { + o2 := new(QueryOptions) + if o != nil { + *o2 = *o + } + o2.ctx = ctx + return o2 +} + +// Context returns the context used for canceling HTTP requests related to this write +func (o *WriteOptions) Context() context.Context { + if o != nil && o.ctx != nil { + return o.ctx + } + return context.Background() +} + +// WithContext creates a copy of the write options using the provided context to cancel related HTTP requests +func (o *WriteOptions) WithContext(ctx context.Context) *WriteOptions { + o2 := new(WriteOptions) + if o != nil { + *o2 = *o + } + o2.ctx = ctx + return o2 +}