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  }