forked from golang/build
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.go
291 lines (265 loc) · 7.54 KB
/
client.go
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package perfdata contains a client for the performance data storage server.
package perfdata
import (
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/perf/storage/benchfmt"
)
// A Client issues queries to a performance data storage server.
// It is safe to use from multiple goroutines simultaneously.
type Client struct {
// BaseURL is the base URL of the storage server.
BaseURL string
// HTTPClient is the HTTP client for sending requests. If nil, http.DefaultClient will be used.
HTTPClient *http.Client
}
// httpClient returns the http.Client to use for requests.
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
// Query searches for results matching the given query string. The
// result is a stream of bytes containing text benchmark data. This data
// may be parsed and processed by the x/perf/benchfmt package.
//
// The query string is first parsed into quoted words (as in the shell)
// and then each word must be formatted as one of the following:
// key:value - exact match on label "key" = "value"
// key>value - value greater than (useful for dates)
// key<value - value less than (also useful for dates)
func (c *Client) Query(ctx context.Context, q string) (io.ReadCloser, error) {
hc := c.httpClient()
resp, err := ctxhttp.Get(ctx, hc, c.BaseURL+"/search?"+url.Values{"q": []string{q}}.Encode())
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("%s", body)
}
return resp.Body, nil
}
// UploadInfo represents an upload summary.
type UploadInfo struct {
Count int
UploadID string
LabelValues benchfmt.Labels `json:",omitempty"`
}
// ListUploads searches for uploads containing results matching the given query string.
// The query may be empty, in which case all uploads will be returned.
// extraLabels specifies other labels to be retrieved.
// If limit is 0, no limit will be provided to the server.
// The uploads are returned starting with the most recent upload.
func (c *Client) ListUploads(ctx context.Context, q string, extraLabels []string, limit int) *UploadList {
hc := c.httpClient()
v := url.Values{"extra_label": extraLabels}
if q != "" {
v["q"] = []string{q}
}
if limit != 0 {
v["limit"] = []string{fmt.Sprintf("%d", limit)}
}
u := c.BaseURL + "/uploads"
if len(v) > 0 {
u += "?" + v.Encode()
}
resp, err := ctxhttp.Get(ctx, hc, u)
if err != nil {
return &UploadList{err: err}
}
if resp.StatusCode != 200 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return &UploadList{err: err}
}
return &UploadList{err: fmt.Errorf("%s", body)}
}
return &UploadList{body: resp.Body, dec: json.NewDecoder(resp.Body)}
}
// UploadList is the result of ListUploads.
// Use Next to advance through the rows, making sure to call Close when done:
//
// q := db.ListUploads("key:value")
// defer q.Close()
// for q.Next() {
// id, count := q.Row()
// labels := q.LabelValues()
// ...
// }
// err = q.Err() // get any error encountered during iteration
// ...
type UploadList struct {
body io.Closer
dec *json.Decoder
// from last call to Next
ui UploadInfo
err error
}
// Next prepares the next result for reading with the Result
// method. It returns false when there are no more results, either by
// reaching the end of the input or an error.
func (ul *UploadList) Next() bool {
if ul.err != nil {
return false
}
// Clear UploadInfo before decoding new value.
ul.ui = UploadInfo{}
ul.err = ul.dec.Decode(&ul.ui)
return ul.err == nil
}
// Info returns the most recent UploadInfo generated by a call to Next.
func (ul *UploadList) Info() UploadInfo {
return ul.ui
}
// Err returns the error state of the query.
func (ul *UploadList) Err() error {
if ul.err == io.EOF {
return nil
}
return ul.err
}
// Close frees resources associated with the query.
func (ul *UploadList) Close() error {
if ul.body != nil {
err := ul.body.Close()
ul.body = nil
return err
}
return ul.Err()
}
// NewUpload starts a new upload to the storage server.
// The upload must have Abort or Commit called on it.
// If the server requires authentication for uploads, c.HTTPClient should be set to the result of oauth2.NewClient.
func (c *Client) NewUpload(ctx context.Context) *Upload {
hc := c.httpClient()
pr, pw := io.Pipe()
mpw := multipart.NewWriter(pw)
req, err := http.NewRequest("POST", c.BaseURL+"/upload", pr)
if err != nil {
return &Upload{err: err}
}
req.Header.Set("Content-Type", mpw.FormDataContentType())
req.Header.Set("User-Agent", "golang.org/x/build/perfdata")
errCh := make(chan error)
u := &Upload{pw: pw, mpw: mpw, errCh: errCh}
go func() {
resp, err := ctxhttp.Do(ctx, hc, req)
if err != nil {
errCh <- err
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
errCh <- fmt.Errorf("upload failed: %v\n%s", resp.Status, body)
return
}
status := &UploadStatus{}
if err := json.NewDecoder(resp.Body).Decode(status); err != nil {
errCh <- err
}
u.status = status
errCh <- nil
}()
return u
}
// UploadStatus contains information about a successful upload.
type UploadStatus struct {
// UploadID is the upload ID assigned to the upload.
UploadID string `json:"uploadid"`
// FileIDs is the list of file IDs assigned to the files in the upload.
FileIDs []string `json:"fileids"`
// ViewURL is a server-supplied URL to view the results.
ViewURL string `json:"viewurl"`
}
// An Upload is an in-progress upload.
// Use CreateFile to upload one or more files, then call Commit or Abort.
//
// u := client.NewUpload()
// w, err := u.CreateFile()
// if err != nil {
// u.Abort()
// return err
// }
// fmt.Fprintf(w, "BenchmarkResult 1 1 ns/op\n")
// if err := u.Commit(); err != nil {
// return err
// }
type Upload struct {
pw io.WriteCloser
mpw *multipart.Writer
status *UploadStatus
// errCh is used to report the success/failure of the HTTP request
errCh chan error
// err is the first observed error; it is only accessed from user-called methods for thread safety
err error
}
// CreateFile creates a new upload with the given name.
// The Writer may be used until CreateFile is called again.
// name may be the empty string if the file does not have a name.
func (u *Upload) CreateFile(name string) (io.Writer, error) {
if u.err != nil {
return nil, u.err
}
return u.mpw.CreateFormFile("file", name)
}
// Commit attempts to commit the upload.
func (u *Upload) Commit() (*UploadStatus, error) {
if u.err != nil {
return nil, u.err
}
if u.err = u.mpw.WriteField("commit", "1"); u.err != nil {
u.Abort()
return nil, u.err
}
if u.err = u.mpw.Close(); u.err != nil {
u.Abort()
return nil, u.err
}
u.mpw = nil
if u.err = u.pw.Close(); u.err != nil {
u.Abort()
return nil, u.err
}
u.pw = nil
u.err = <-u.errCh
u.errCh = nil
if u.err != nil {
return nil, u.err
}
return u.status, nil
}
// Abort attempts to cancel the in-progress upload.
func (u *Upload) Abort() error {
if u.mpw != nil {
u.mpw.WriteField("abort", "1")
// Writing the 'abort' field will cause the server to send back an error response.
u.mpw.Close()
u.mpw = nil
}
if u.pw != nil {
u.pw.Close()
u.pw = nil
}
err := <-u.errCh
u.errCh = nil
if u.err == nil {
u.err = err
}
return u.err
}