Skip to content

Commit 13da401

Browse files
service_client: Accept per-call Context
Adds the ability to pass per-call context.Context. Adds a WithContext method to the ServiceClient, which will return a new ServiceClient. The context added to ServiceClient will be used in addition to the ProviderClient's Context to make HTTP calls. Example use: ```Go ctx := context.Background() allPages, err := servers.List(client.WithContext(ctx), servers.ListOpts{}).AllPages() // the original `client` is untouched ```
1 parent abb45fc commit 13da401

6 files changed

Lines changed: 664 additions & 30 deletions

File tree

‎context.go‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package gophercloud
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
)
8+
9+
type mergeContext struct {
10+
child context.Context
11+
parent context.Context
12+
ch chan struct{}
13+
14+
sync.Mutex
15+
err error
16+
}
17+
18+
func idempotentlyClose(ch chan struct{}) {
19+
select {
20+
case <-ch:
21+
default:
22+
close(ch)
23+
}
24+
}
25+
26+
// MergeContext returns a copy of ctx that also obeys a second parent.
27+
// MergeContext always returns a non-nil context.Context.
28+
func MergeContext(child, parent context.Context) (context.Context, context.CancelFunc) {
29+
if child == nil {
30+
if parent == nil {
31+
return context.WithCancel(context.Background())
32+
}
33+
return context.WithCancel(parent)
34+
}
35+
if parent == nil {
36+
return context.WithCancel(child)
37+
}
38+
39+
ch := make(chan struct{})
40+
cancelCh := make(chan struct{})
41+
42+
go func() {
43+
select {
44+
case <-child.Done():
45+
case <-parent.Done():
46+
case <-cancelCh:
47+
}
48+
close(ch)
49+
}()
50+
51+
return &mergeContext{
52+
child: child,
53+
parent: parent,
54+
ch: ch,
55+
}, func() { idempotentlyClose(cancelCh) }
56+
}
57+
58+
// Value returns Context's value for the key, or parent's if nil.
59+
func (ctx *mergeContext) Value(key interface{}) interface{} {
60+
if v := ctx.child.Value(key); v != nil {
61+
return v
62+
}
63+
return ctx.parent.Value(key)
64+
}
65+
66+
// Done returns a channel that is closed when either Context's or parent's is.
67+
func (ctx *mergeContext) Done() <-chan struct{} {
68+
return ctx.ch
69+
}
70+
71+
// Err returns Context's Err(), or parent's if nil. After Err returns a
72+
// non-nil error, successive calls to Err return the same error.
73+
func (ctx *mergeContext) Err() error {
74+
ctx.Lock()
75+
defer ctx.Unlock()
76+
77+
if ctx.err == nil {
78+
if err := ctx.child.Err(); err != nil {
79+
ctx.err = err
80+
} else {
81+
ctx.err = ctx.parent.Err()
82+
}
83+
}
84+
return ctx.err
85+
}
86+
87+
// Deadline returns the closest deadline, if any.
88+
func (ctx *mergeContext) Deadline() (deadline time.Time, ok bool) {
89+
if d1, ok := ctx.child.Deadline(); ok {
90+
if d2, ok := ctx.parent.Deadline(); ok {
91+
if d1.Before(d2) {
92+
return d1, ok
93+
} else {
94+
return d2, ok
95+
}
96+
}
97+
return d1, ok
98+
}
99+
return ctx.parent.Deadline()
100+
}

‎provider_client.go‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ type RequestOpts struct {
335335
// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
336336
// response body is considered for further use. Valid when JSONResponse is nil.
337337
KeepResponseBody bool
338+
// Context will be merged to the ProviderClient's Context to make HTTP request.
339+
Context context.Context
338340
}
339341

340342
// requestState contains temporary state for a single ProviderClient.Request() call.
@@ -362,6 +364,9 @@ func (client *ProviderClient) doRequest(method, url string, options *RequestOpts
362364
var body io.Reader
363365
var contentType *string
364366

367+
ctx, cancelFunc := MergeContext(options.Context, client.Context)
368+
defer cancelFunc()
369+
365370
// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
366371
// io.ReadSeeker as-is. Default the content-type to application/json.
367372
if options.JSONBody != nil {
@@ -388,13 +393,10 @@ func (client *ProviderClient) doRequest(method, url string, options *RequestOpts
388393
}
389394

390395
// Construct the http.Request.
391-
req, err := http.NewRequest(method, url, body)
396+
req, err := http.NewRequestWithContext(ctx, method, url, body)
392397
if err != nil {
393398
return nil, err
394399
}
395-
if client.Context != nil {
396-
req = req.WithContext(client.Context)
397-
}
398400

399401
// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
400402
// modify or omit any header.
@@ -425,7 +427,7 @@ func (client *ProviderClient) doRequest(method, url string, options *RequestOpts
425427
if client.RetryFunc != nil {
426428
var e error
427429
state.retries = state.retries + 1
428-
e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
430+
e = client.RetryFunc(ctx, method, url, options, err, state.retries)
429431
if e != nil {
430432
return nil, e
431433
}
@@ -543,7 +545,7 @@ func (client *ProviderClient) doRequest(method, url string, options *RequestOpts
543545
var e error
544546

545547
state.retries = state.retries + 1
546-
e = f(client.Context, &respErr, err, state.retries)
548+
e = f(ctx, &respErr, err, state.retries)
547549

548550
if e != nil {
549551
return resp, e
@@ -570,7 +572,7 @@ func (client *ProviderClient) doRequest(method, url string, options *RequestOpts
570572
if err != nil && client.RetryFunc != nil {
571573
var e error
572574
state.retries = state.retries + 1
573-
e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
575+
e = client.RetryFunc(ctx, method, url, options, err, state.retries)
574576
if e != nil {
575577
return resp, e
576578
}
@@ -594,7 +596,7 @@ func (client *ProviderClient) doRequest(method, url string, options *RequestOpts
594596
if client.RetryFunc != nil {
595597
var e error
596598
state.retries = state.retries + 1
597-
e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
599+
e = client.RetryFunc(ctx, method, url, options, err, state.retries)
598600
if e != nil {
599601
return resp, e
600602
}

‎service_client.go‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gophercloud
22

33
import (
4+
"context"
45
"io"
56
"net/http"
67
"strings"
@@ -32,6 +33,30 @@ type ServiceClient struct {
3233
// MoreHeaders allows users (or Gophercloud) to set service-wide headers on requests. Put another way,
3334
// values set in this field will be set on all the HTTP requests the service client sends.
3435
MoreHeaders map[string]string
36+
37+
// Context will merge with the ProviderClient's before being used in HTTP requests
38+
Context context.Context
39+
}
40+
41+
func (client *ServiceClient) copy(options ...func(*ServiceClient)) *ServiceClient {
42+
newClient := *client
43+
newClient.MoreHeaders = make(map[string]string)
44+
for k, v := range client.MoreHeaders {
45+
newClient.MoreHeaders[k] = v
46+
}
47+
48+
for _, apply := range options {
49+
apply(&newClient)
50+
}
51+
52+
return &newClient
53+
}
54+
55+
// WithContext returns a copy of ServiceClient with the provided Context.
56+
func (client *ServiceClient) WithContext(ctx context.Context) *ServiceClient {
57+
return client.copy(func(c *ServiceClient) {
58+
c.Context = ctx
59+
})
3560
}
3661

3762
// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
@@ -150,6 +175,11 @@ func (client *ServiceClient) Request(method, url string, options *RequestOpts) (
150175
options.MoreHeaders[k] = v
151176
}
152177
}
178+
179+
if client.Context != nil {
180+
options.Context = client.Context
181+
}
182+
153183
return client.ProviderClient.Request(method, url, options)
154184
}
155185

0 commit comments

Comments
 (0)