File: do.go

package info (click to toggle)
golang-github-jacobsa-gcloud 0.0~git20150709-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, buster, forky, sid, trixie
  • size: 484 kB
  • sloc: makefile: 3
file content (129 lines) | stat: -rw-r--r-- 3,487 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httputil

import (
	"fmt"
	"io"
	"net/http"
	"reflect"
	"time"

	"golang.org/x/net/context"
)

// Wait for the allDone channel to be closed. If the context is cancelled
// before then, cancel the HTTP request via the canceller repeatedly until
// allDone is closed.
//
// We must cancel repeatedly because the http package's design for cancellation
// is flawed: the canceller must naturally race with the transport receiving
// the request (CancelRequest may be called before RoundTrip, and in this case
// the cancellation will be lost). This is especially a problem with wrapper
// transports like the oauth2 one, which may do extra work before calling
// through to http.Transport.
func propagateCancellation(
	ctx context.Context,
	allDone chan struct{},
	c CancellableRoundTripper,
	req *http.Request) {
	// If the context is not cancellable, there is nothing interesting we can do.
	cancelChan := ctx.Done()
	if cancelChan == nil {
		return
	}

	// If the user closes allDone before the context is cancelled, we can return
	// immediately.
	select {
	case <-allDone:
		return

	case <-cancelChan:
	}

	// The context has been cancelled. Repeatedly cancel the HTTP request as
	// described above until the user closes allDone.
	for {
		c.CancelRequest(req)

		select {
		case <-allDone:
			return

		case <-time.After(10 * time.Millisecond):
		}
	}
}

// An io.ReadCloser that closes a channel when it is closed. Used to clean up
// the propagateCancellation helper.
type closeChannelReadCloser struct {
	wrapped io.ReadCloser
	c       chan struct{}
}

func (rc *closeChannelReadCloser) Read(p []byte) (n int, err error) {
	n, err = rc.wrapped.Read(p)
	return
}

func (rc *closeChannelReadCloser) Close() (err error) {
	if rc.c != nil {
		close(rc.c)
		rc.c = nil
	}

	err = rc.wrapped.Close()
	return
}

// Call client.Do with the supplied request, cancelling the request if the
// context is cancelled. Return an error if the client does not support
// cancellation.
func Do(
	ctx context.Context,
	client *http.Client,
	req *http.Request) (resp *http.Response, err error) {
	// Make sure the transport supports cancellation.
	c, ok := client.Transport.(CancellableRoundTripper)
	if !ok {
		err = fmt.Errorf(
			"Transport of type %v doesn't support cancellation",
			reflect.TypeOf(client.Transport))
		return
	}

	// Set up a goroutine that will watch the context for cancellation, and a
	// channel that tells it that it can return.
	allDone := make(chan struct{})
	go propagateCancellation(ctx, allDone, c, req)

	// Call through. On error, clean up the helper function.
	resp, err = client.Do(req)
	if err != nil {
		close(allDone)
		return
	}

	// Otherwise, wrap the body in a layer that will tell the helper function
	// that it can return when it is closed.
	resp.Body = &closeChannelReadCloser{
		wrapped: resp.Body,
		c:       allDone,
	}

	return
}