github.com/jgbaldwinbrown/perf@v0.1.1/storage/client.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package storage contains a client for the performance data storage server. 6 // 7 // Deprecated: Moved to golang.org/x/build/perfdata. 8 package storage 9 10 import ( 11 "encoding/json" 12 "fmt" 13 "io" 14 "mime/multipart" 15 "net/http" 16 "net/url" 17 18 "golang.org/x/net/context" 19 "golang.org/x/net/context/ctxhttp" 20 "golang.org/x/perf/storage/benchfmt" 21 ) 22 23 // A Client issues queries to a performance data storage server. 24 // It is safe to use from multiple goroutines simultaneously. 25 type Client struct { 26 // BaseURL is the base URL of the storage server. 27 BaseURL string 28 // HTTPClient is the HTTP client for sending requests. If nil, http.DefaultClient will be used. 29 HTTPClient *http.Client 30 } 31 32 // httpClient returns the http.Client to use for requests. 33 func (c *Client) httpClient() *http.Client { 34 if c.HTTPClient != nil { 35 return c.HTTPClient 36 } 37 return http.DefaultClient 38 } 39 40 // Query searches for results matching the given query string. 41 // 42 // The query string is first parsed into quoted words (as in the shell) 43 // and then each word must be formatted as one of the following: 44 // key:value - exact match on label "key" = "value" 45 // key>value - value greater than (useful for dates) 46 // key<value - value less than (also useful for dates) 47 func (c *Client) Query(ctx context.Context, q string) *Query { 48 hc := c.httpClient() 49 50 resp, err := ctxhttp.Get(ctx, hc, c.BaseURL+"/search?"+url.Values{"q": []string{q}}.Encode()) 51 if err != nil { 52 return &Query{err: err} 53 } 54 if resp.StatusCode != 200 { 55 defer resp.Body.Close() 56 body, err := io.ReadAll(resp.Body) 57 if err != nil { 58 return &Query{err: err} 59 } 60 return &Query{err: fmt.Errorf("%s", body)} 61 } 62 63 br := benchfmt.NewReader(resp.Body) 64 65 return &Query{br: br, body: resp.Body} 66 } 67 68 // A Query allows iteration over the results of a search query. 69 // Use Next to advance through the results, making sure to call Close when done: 70 // 71 // q := client.Query("key:value") 72 // defer q.Close() 73 // for q.Next() { 74 // res := q.Result() 75 // ... 76 // } 77 // if err = q.Err(); err != nil { 78 // // handle error encountered during query 79 // } 80 type Query struct { 81 br *benchfmt.Reader 82 body io.ReadCloser 83 err error 84 } 85 86 // Next prepares the next result for reading with the Result 87 // method. It returns false when there are no more results, either by 88 // reaching the end of the input or an error. 89 func (q *Query) Next() bool { 90 if q.err != nil { 91 return false 92 } 93 return q.br.Next() 94 } 95 96 // Result returns the most recent result generated by a call to Next. 97 func (q *Query) Result() *benchfmt.Result { 98 return q.br.Result() 99 } 100 101 // Err returns the first error encountered during the query. 102 func (q *Query) Err() error { 103 if q.err != nil { 104 return q.err 105 } 106 return q.br.Err() 107 } 108 109 // Close frees resources associated with the query. 110 func (q *Query) Close() error { 111 if q.body != nil { 112 q.body.Close() 113 q.body = nil 114 } 115 return q.err 116 } 117 118 // UploadInfo represents an upload summary. 119 type UploadInfo struct { 120 Count int 121 UploadID string 122 LabelValues benchfmt.Labels `json:",omitempty"` 123 } 124 125 // ListUploads searches for uploads containing results matching the given query string. 126 // The query may be empty, in which case all uploads will be returned. 127 // extraLabels specifies other labels to be retrieved. 128 // If limit is 0, no limit will be provided to the server. 129 // The uploads are returned starting with the most recent upload. 130 func (c *Client) ListUploads(ctx context.Context, q string, extraLabels []string, limit int) *UploadList { 131 hc := c.httpClient() 132 133 v := url.Values{"extra_label": extraLabels} 134 if q != "" { 135 v["q"] = []string{q} 136 } 137 if limit != 0 { 138 v["limit"] = []string{fmt.Sprintf("%d", limit)} 139 } 140 141 u := c.BaseURL + "/uploads" 142 if len(v) > 0 { 143 u += "?" + v.Encode() 144 } 145 resp, err := ctxhttp.Get(ctx, hc, u) 146 if err != nil { 147 return &UploadList{err: err} 148 } 149 if resp.StatusCode != 200 { 150 body, err := io.ReadAll(resp.Body) 151 if err != nil { 152 return &UploadList{err: err} 153 } 154 return &UploadList{err: fmt.Errorf("%s", body)} 155 } 156 return &UploadList{body: resp.Body, dec: json.NewDecoder(resp.Body)} 157 } 158 159 // UploadList is the result of ListUploads. 160 // Use Next to advance through the rows, making sure to call Close when done: 161 // 162 // q := db.ListUploads("key:value") 163 // defer q.Close() 164 // for q.Next() { 165 // id, count := q.Row() 166 // labels := q.LabelValues() 167 // ... 168 // } 169 // err = q.Err() // get any error encountered during iteration 170 // ... 171 type UploadList struct { 172 body io.Closer 173 dec *json.Decoder 174 // from last call to Next 175 ui UploadInfo 176 err error 177 } 178 179 // Next prepares the next result for reading with the Result 180 // method. It returns false when there are no more results, either by 181 // reaching the end of the input or an error. 182 func (ul *UploadList) Next() bool { 183 if ul.err != nil { 184 return false 185 } 186 187 // Clear UploadInfo before decoding new value. 188 ul.ui = UploadInfo{} 189 190 ul.err = ul.dec.Decode(&ul.ui) 191 return ul.err == nil 192 } 193 194 // Info returns the most recent UploadInfo generated by a call to Next. 195 func (ul *UploadList) Info() UploadInfo { 196 return ul.ui 197 } 198 199 // Err returns the error state of the query. 200 func (ul *UploadList) Err() error { 201 if ul.err == io.EOF { 202 return nil 203 } 204 return ul.err 205 } 206 207 // Close frees resources associated with the query. 208 func (ul *UploadList) Close() error { 209 if ul.body != nil { 210 err := ul.body.Close() 211 ul.body = nil 212 return err 213 } 214 return ul.Err() 215 } 216 217 // NewUpload starts a new upload to the storage server. 218 // The upload must have Abort or Commit called on it. 219 // If the server requires authentication for uploads, c.HTTPClient should be set to the result of oauth2.NewClient. 220 func (c *Client) NewUpload(ctx context.Context) *Upload { 221 hc := c.httpClient() 222 223 pr, pw := io.Pipe() 224 mpw := multipart.NewWriter(pw) 225 226 req, err := http.NewRequest("POST", c.BaseURL+"/upload", pr) 227 if err != nil { 228 return &Upload{err: err} 229 } 230 req.Header.Set("Content-Type", mpw.FormDataContentType()) 231 req.Header.Set("User-Agent", "golang.org/x/perf/storage") 232 errCh := make(chan error) 233 u := &Upload{pw: pw, mpw: mpw, errCh: errCh} 234 go func() { 235 resp, err := ctxhttp.Do(ctx, hc, req) 236 if err != nil { 237 errCh <- err 238 return 239 } 240 defer resp.Body.Close() 241 if resp.StatusCode != 200 { 242 body, _ := io.ReadAll(resp.Body) 243 errCh <- fmt.Errorf("upload failed: %v\n%s", resp.Status, body) 244 return 245 } 246 status := &UploadStatus{} 247 if err := json.NewDecoder(resp.Body).Decode(status); err != nil { 248 errCh <- err 249 } 250 u.status = status 251 errCh <- nil 252 }() 253 return u 254 } 255 256 // UploadStatus contains information about a successful upload. 257 type UploadStatus struct { 258 // UploadID is the upload ID assigned to the upload. 259 UploadID string `json:"uploadid"` 260 // FileIDs is the list of file IDs assigned to the files in the upload. 261 FileIDs []string `json:"fileids"` 262 // ViewURL is a server-supplied URL to view the results. 263 ViewURL string `json:"viewurl"` 264 } 265 266 // An Upload is an in-progress upload. 267 // Use CreateFile to upload one or more files, then call Commit or Abort. 268 // 269 // u := client.NewUpload() 270 // w, err := u.CreateFile() 271 // if err != nil { 272 // u.Abort() 273 // return err 274 // } 275 // fmt.Fprintf(w, "BenchmarkResult 1 1 ns/op\n") 276 // if err := u.Commit(); err != nil { 277 // return err 278 // } 279 type Upload struct { 280 pw io.WriteCloser 281 mpw *multipart.Writer 282 status *UploadStatus 283 // errCh is used to report the success/failure of the HTTP request 284 errCh chan error 285 // err is the first observed error; it is only accessed from user-called methods for thread safety 286 err error 287 } 288 289 // CreateFile creates a new upload with the given name. 290 // The Writer may be used until CreateFile is called again. 291 // name may be the empty string if the file does not have a name. 292 func (u *Upload) CreateFile(name string) (io.Writer, error) { 293 if u.err != nil { 294 return nil, u.err 295 } 296 return u.mpw.CreateFormFile("file", name) 297 } 298 299 // Commit attempts to commit the upload. 300 func (u *Upload) Commit() (*UploadStatus, error) { 301 if u.err != nil { 302 return nil, u.err 303 } 304 if u.err = u.mpw.WriteField("commit", "1"); u.err != nil { 305 u.Abort() 306 return nil, u.err 307 } 308 if u.err = u.mpw.Close(); u.err != nil { 309 u.Abort() 310 return nil, u.err 311 } 312 u.mpw = nil 313 if u.err = u.pw.Close(); u.err != nil { 314 u.Abort() 315 return nil, u.err 316 } 317 u.pw = nil 318 u.err = <-u.errCh 319 u.errCh = nil 320 if u.err != nil { 321 return nil, u.err 322 } 323 return u.status, nil 324 } 325 326 // Abort attempts to cancel the in-progress upload. 327 func (u *Upload) Abort() error { 328 if u.mpw != nil { 329 u.mpw.WriteField("abort", "1") 330 // Writing the 'abort' field will cause the server to send back an error response. 331 u.mpw.Close() 332 u.mpw = nil 333 } 334 if u.pw != nil { 335 u.pw.Close() 336 u.pw = nil 337 } 338 err := <-u.errCh 339 u.errCh = nil 340 if u.err == nil { 341 u.err = err 342 } 343 return u.err 344 }