-
Notifications
You must be signed in to change notification settings - Fork 75
Expand file tree
/
Copy pathcode_intel_upload_vendored.go
More file actions
634 lines (533 loc) · 20.8 KB
/
Copy pathcode_intel_upload_vendored.go
File metadata and controls
634 lines (533 loc) · 20.8 KB
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
package main
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"sync"
"time"
"github.com/sourcegraph/conc/pool"
"github.com/sourcegraph/sourcegraph/lib/codeintel/upload"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"
)
/*
NOTE:
All the definitions below have been vendored in from the Sourcegraph repository public snapshot at
this commit: https://github.com/sourcegraph/sourcegraph-public-snapshot/commit/1af563b61442c255af7b07a526efd71b3b0bad0d
All necessary definitions were vendored in without changes until the code built successfully.
*/
// uploadIndex uploads the index file described by the given options to a Sourcegraph
// instance via a single HTTP POST request. The identifier of the upload is returned
// after a successful upload.
func uploadIndex(ctx context.Context, httpClient upload.Client, opts upload.UploadOptions, r io.ReaderAt, readerLen, uncompressedSize int64) (id int, err error) {
bars := []output.ProgressBar{{Label: "Upload", Max: 1.0}}
progress, retry, complete := logProgress(
opts.Output,
bars,
"Index uploaded",
"Failed to upload index file",
)
defer func() { complete(err) }()
// Create a section reader that can reset our reader view for retries
reader := io.NewSectionReader(r, 0, readerLen)
requestOptions := uploadRequestOptions{
UploadOptions: opts,
Target: &id,
UncompressedSize: uncompressedSize,
}
err = uploadIndexFile(ctx, httpClient, opts, reader, readerLen, requestOptions, progress, retry, 0, 1)
if progress != nil {
// Mark complete in case we debounced our last updates
progress.SetValue(0, 1)
}
return id, err
}
// uploadIndexFile uploads the contents available via the given reader to a
// Sourcegraph instance with the given request options.i
func uploadIndexFile(ctx context.Context, httpClient upload.Client, uploadOptions upload.UploadOptions, reader io.ReadSeeker, readerLen int64, requestOptions uploadRequestOptions, progress output.Progress, retry onRetryLogFn, barIndex int, numParts int) error {
retrier := makeRetry(uploadOptions.MaxRetries, uploadOptions.RetryInterval)
return retrier(func(attempt int) (_ bool, err error) {
defer func() {
if err != nil && !errors.Is(err, ctx.Err()) && progress != nil {
progress.SetValue(barIndex, 0)
}
}()
if attempt != 0 {
suffix := ""
if numParts != 1 {
suffix = fmt.Sprintf(" %d of %d", barIndex+1, numParts)
}
if progress != nil {
progress.SetValue(barIndex, 0)
}
progress = retry(fmt.Sprintf("Failed to upload index file%s (will retry; attempt #%d)", suffix, attempt))
}
// Create fresh reader on each attempt
reader.Seek(0, io.SeekStart)
// Report upload progress as writes occur
requestOptions.Payload = newProgressCallbackReader(reader, readerLen, progress, barIndex)
// Perform upload
return performUploadRequest(ctx, httpClient, requestOptions)
})
}
// uploadMultipartIndex uploads the index file described by the given options to a
// Sourcegraph instance over multiple HTTP POST requests. The identifier of the upload
// is returned after a successful upload.
func uploadMultipartIndex(ctx context.Context, httpClient upload.Client, opts upload.UploadOptions, r io.ReaderAt, readerLen, uncompressedSize int64) (_ int, err error) {
// Create a slice of section readers for upload part retries.
// This allows us to both read concurrently from the same reader,
// but also retry reads from arbitrary offsets.
readers := splitReader(r, readerLen, opts.MaxPayloadSizeBytes)
// Perform initial request that gives us our upload identifier
id, err := uploadMultipartIndexInit(ctx, httpClient, opts, len(readers), uncompressedSize)
if err != nil {
return 0, err
}
// Upload each payload of the multipart index
if err := uploadMultipartIndexParts(ctx, httpClient, opts, readers, id, readerLen); err != nil {
return 0, err
}
// Finalize the upload and mark it as ready for processing
if err := uploadMultipartIndexFinalize(ctx, httpClient, opts, id); err != nil {
return 0, err
}
return id, nil
}
// uploadMultipartIndexInit performs an initial request to prepare the backend to accept upload
// parts via additional HTTP requests. This upload will be in a pending state until all upload
// parts are received and the multipart upload is finalized, or until the record is deleted by
// a background process after an expiry period.
func uploadMultipartIndexInit(ctx context.Context, httpClient upload.Client, opts upload.UploadOptions, numParts int, uncompressedSize int64) (id int, err error) {
retry, complete := logPending(
opts.Output,
"Preparing multipart upload",
"Prepared multipart upload",
"Failed to prepare multipart upload",
)
defer func() { complete(err) }()
err = makeRetry(opts.MaxRetries, opts.RetryInterval)(func(attempt int) (bool, error) {
if attempt != 0 {
retry(fmt.Sprintf("Failed to prepare multipart upload (will retry; attempt #%d)", attempt))
}
return performUploadRequest(ctx, httpClient, uploadRequestOptions{
UploadOptions: opts,
Target: &id,
MultiPart: true,
NumParts: numParts,
UncompressedSize: uncompressedSize,
})
})
return id, err
}
// uploadMultipartIndexParts uploads the contents available via each of the given reader(s)
// to a Sourcegraph instance as part of the same multipart upload as indiciated
// by the given identifier.
func uploadMultipartIndexParts(ctx context.Context, httpClient upload.Client, opts upload.UploadOptions, readers []io.ReadSeeker, id int, readerLen int64) (err error) {
var bars []output.ProgressBar
for i := range readers {
label := fmt.Sprintf("Upload part %d of %d", i+1, len(readers))
bars = append(bars, output.ProgressBar{Label: label, Max: 1.0})
}
progress, retry, complete := logProgress(
opts.Output,
bars,
"Index parts uploaded",
"Failed to upload index parts",
)
defer func() { complete(err) }()
pool := new(pool.ErrorPool).WithFirstError().WithContext(ctx)
if opts.MaxConcurrency > 0 {
pool.WithMaxGoroutines(opts.MaxConcurrency)
}
for i, reader := range readers {
pool.Go(func(ctx context.Context) error {
// Determine size of this reader. If we're not the last reader in the slice,
// then we're the maximum payload size. Otherwise, we're whatever is left.
partReaderLen := opts.MaxPayloadSizeBytes
if i == len(readers)-1 {
partReaderLen = readerLen - int64(len(readers)-1)*opts.MaxPayloadSizeBytes
}
requestOptions := uploadRequestOptions{
UploadOptions: opts,
UploadID: id,
Index: i,
}
if err := uploadIndexFile(ctx, httpClient, opts, reader, partReaderLen, requestOptions, progress, retry, i, len(readers)); err != nil {
return err
} else if progress != nil {
// Mark complete in case we debounced our last updates
progress.SetValue(i, 1)
}
return nil
})
}
return pool.Wait()
}
// uploadMultipartIndexFinalize performs the request to stitch the uploaded parts together and
// mark it ready as processing in the backend.
func uploadMultipartIndexFinalize(ctx context.Context, httpClient upload.Client, opts upload.UploadOptions, id int) (err error) {
retry, complete := logPending(
opts.Output,
"Finalizing multipart upload",
"Finalized multipart upload",
"Failed to finalize multipart upload",
)
defer func() { complete(err) }()
return makeRetry(opts.MaxRetries, opts.RetryInterval)(func(attempt int) (bool, error) {
if attempt != 0 {
retry(fmt.Sprintf("Failed to finalize multipart upload (will retry; attempt #%d)", attempt))
}
return performUploadRequest(ctx, httpClient, uploadRequestOptions{
UploadOptions: opts,
UploadID: id,
Done: true,
})
})
}
// splitReader returns a slice of read-seekers into the input ReaderAt, each of max size maxPayloadSize.
//
// The sequential concatenation of each reader produces the content of the original reader.
//
// Each reader is safe to use concurrently with others. The original reader should be closed when all produced
// readers are no longer active.
func splitReader(r io.ReaderAt, n, maxPayloadSize int64) (readers []io.ReadSeeker) {
for offset := int64(0); offset < n; offset += maxPayloadSize {
readers = append(readers, io.NewSectionReader(r, offset, maxPayloadSize))
}
return readers
}
// openFileAndGetSize returns an open file handle and the size on disk for the given filename.
func openFileAndGetSize(filename string) (*os.File, int64, error) {
fileInfo, err := os.Stat(filename)
if err != nil {
return nil, 0, err
}
file, err := os.Open(filename)
if err != nil {
return nil, 0, err
}
return file, fileInfo.Size(), err
}
// logPending creates a pending object from the given output value and returns a retry function that
// can be called to print a message then reset the pending display, and a complete function that should
// be called once the work attached to this log call has completed. This complete function takes an error
// value that determines whether the success or failure message is displayed. If the given output value is
// nil then a no-op complete function is returned.
func logPending(out *output.Output, pendingMessage, successMessage, failureMessage string) (func(message string), func(error)) {
if out == nil {
return func(message string) {}, func(err error) {}
}
pending := out.Pending(output.Line("", output.StylePending, pendingMessage))
retry := func(message string) {
pending.Destroy()
out.WriteLine(output.Line(output.EmojiFailure, output.StyleReset, message))
pending = out.Pending(output.Line("", output.StylePending, pendingMessage))
}
complete := func(err error) {
if err == nil {
pending.Complete(output.Line(output.EmojiSuccess, output.StyleSuccess, successMessage))
} else {
pending.Complete(output.Line(output.EmojiFailure, output.StyleBold, failureMessage))
}
}
return retry, complete
}
type onRetryLogFn func(message string) output.Progress
// logProgress creates and returns a progress from the given output value and bars configuration.
// This function also returns a retry function that can be called to print a message then reset the
// progress bar display, and a complete function that should be called once the work attached to
// this log call has completed. This complete function takes an error value that determines whether
// the success or failure message is displayed. If the given output value is nil then a no-op complete
// function is returned.
func logProgress(out *output.Output, bars []output.ProgressBar, successMessage, failureMessage string) (output.Progress, onRetryLogFn, func(error)) {
if out == nil {
return nil, func(message string) output.Progress { return nil }, func(err error) {}
}
var mu sync.Mutex
progress := out.Progress(bars, nil)
retry := func(message string) output.Progress {
mu.Lock()
defer mu.Unlock()
progress.Destroy()
out.WriteLine(output.Line(output.EmojiFailure, output.StyleReset, message))
progress = out.Progress(bars, nil)
return progress
}
complete := func(err error) {
progress.Destroy()
if err == nil {
out.WriteLine(output.Line(output.EmojiSuccess, output.StyleSuccess, successMessage))
} else {
out.WriteLine(output.Line(output.EmojiFailure, output.StyleBold, failureMessage))
}
}
return progress, retry, complete
}
type uploadRequestOptions struct {
upload.UploadOptions
Payload io.Reader // Request payload
Target *int // Pointer to upload id decoded from resp
MultiPart bool // Whether the request is a multipart init
NumParts int // The number of upload parts
UncompressedSize int64 // The uncompressed size of the upload
UploadID int // The multipart upload ID
Index int // The index part being uploaded
Done bool // Whether the request is a multipart finalize
}
// ErrUnexpectedStatusCode is returned for HTTP error responses from the upload
// endpoint. It carries the status code and response body so callers can inspect
// them without parsing the error string.
type ErrUnexpectedStatusCode struct {
Code int
Body string
}
func (e *ErrUnexpectedStatusCode) Error() string {
if e.Body != "" {
return fmt.Sprintf("unexpected status code: %d (%s)", e.Code, e.Body)
}
return fmt.Sprintf("unexpected status code: %d", e.Code)
}
// performUploadRequest performs an HTTP POST to the upload endpoint. The query string of the request
// is constructed from the given request options and the body of the request is the unmodified reader.
// If target is a non-nil pointer, it will be assigned the value of the upload identifier present
// in the response body. This function returns an error as well as a boolean flag indicating if the
// function can be retried.
func performUploadRequest(ctx context.Context, httpClient upload.Client, opts uploadRequestOptions) (bool, error) {
req, err := makeUploadRequest(opts)
if err != nil {
return false, err
}
// body closed as part of performRequest
//nolint:bodyclose
resp, body, err := performRequest(ctx, req, httpClient, opts.OutputOptions.Logger)
if err != nil {
return false, err
}
return decodeUploadPayload(resp, body, opts.Target)
}
// makeUploadRequest creates an HTTP request to the upload endpoint described by the given arguments.
func makeUploadRequest(opts uploadRequestOptions) (*http.Request, error) {
uploadURL, err := makeUploadURL(opts)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", uploadURL.String(), opts.Payload)
if err != nil {
return nil, err
}
if opts.UncompressedSize != 0 {
req.Header.Set("X-Uncompressed-Size", strconv.Itoa(int(opts.UncompressedSize)))
}
if opts.SourcegraphInstanceOptions.AccessToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("token %s", opts.SourcegraphInstanceOptions.AccessToken))
}
for k, v := range opts.SourcegraphInstanceOptions.AdditionalHeaders {
req.Header.Set(k, v)
}
return req, nil
}
// performRequest performs an HTTP request and returns the HTTP response as well as the entire
// body as a byte slice. If a logger is supplied, the request, response, and response body will
// be logged.
func performRequest(ctx context.Context, req *http.Request, httpClient upload.Client, logger upload.RequestLogger) (*http.Response, []byte, error) {
started := time.Now()
if logger != nil {
logger.LogRequest(req)
}
resp, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if logger != nil {
logger.LogResponse(req, resp, body, time.Since(started))
}
if err != nil {
return nil, nil, err
}
return resp, body, nil
}
// decodeUploadPayload reads the given response to an upload request. If target is a non-nil pointer,
// it will be assigned the value of the upload identifier present in the response body. This function
// returns a boolean flag indicating if the function can be retried on failure (error-dependent).
func decodeUploadPayload(resp *http.Response, body []byte, target *int) (bool, error) {
if resp.StatusCode >= 300 {
detail := ""
if trimmed := bytes.TrimSpace(body); len(trimmed) > 0 && trimmed[0] != '<' {
detail = string(trimmed)
}
// Do not retry client errors
return resp.StatusCode >= 500, &ErrUnexpectedStatusCode{Code: resp.StatusCode, Body: detail}
}
if target == nil {
// No target expected, skip decoding body
return false, nil
}
var respPayload struct {
ID string `json:"id"`
}
if err := json.Unmarshal(body, &respPayload); err != nil {
return false, errors.Errorf("unexpected response (%s)", err)
}
id, err := strconv.Atoi(respPayload.ID)
if err != nil {
return false, errors.Errorf("unexpected response (%s)", err)
}
*target = id
return false, nil
}
// makeUploadURL creates a URL pointing to the configured Sourcegraph upload
// endpoint with the query string described by the given request options.
func makeUploadURL(opts uploadRequestOptions) (*url.URL, error) {
qs := url.Values{}
if opts.SourcegraphInstanceOptions.GitHubToken != "" {
qs.Add("github_token", opts.SourcegraphInstanceOptions.GitHubToken)
}
if opts.SourcegraphInstanceOptions.GitLabToken != "" {
qs.Add("gitlab_token", opts.SourcegraphInstanceOptions.GitLabToken)
}
if opts.UploadRecordOptions.Repo != "" {
qs.Add("repository", opts.UploadRecordOptions.Repo)
}
if opts.UploadRecordOptions.Commit != "" {
qs.Add("commit", opts.UploadRecordOptions.Commit)
}
if opts.UploadRecordOptions.Root != "" {
qs.Add("root", opts.UploadRecordOptions.Root)
}
if opts.UploadRecordOptions.Indexer != "" {
qs.Add("indexerName", opts.UploadRecordOptions.Indexer)
}
if opts.UploadRecordOptions.IndexerVersion != "" {
qs.Add("indexerVersion", opts.UploadRecordOptions.IndexerVersion)
}
if opts.UploadRecordOptions.AssociatedIndexID != nil {
qs.Add("associatedIndexId", formatInt(*opts.UploadRecordOptions.AssociatedIndexID))
}
if opts.MultiPart {
qs.Add("multiPart", "true")
}
if opts.NumParts != 0 {
qs.Add("numParts", formatInt(opts.NumParts))
}
if opts.UploadID != 0 {
qs.Add("uploadId", formatInt(opts.UploadID))
}
if opts.UploadID != 0 && !opts.MultiPart && !opts.Done {
// Do not set an index of zero unless we're uploading a part
qs.Add("index", formatInt(opts.Index))
}
if opts.Done {
qs.Add("done", "true")
}
path := opts.SourcegraphInstanceOptions.Path
if path == "" {
path = "/.api/scip/upload"
}
parsedUrl, err := url.Parse(opts.SourcegraphInstanceOptions.SourcegraphURL + path)
if err != nil {
return nil, err
}
parsedUrl.RawQuery = qs.Encode()
return parsedUrl, nil
}
func formatInt(v int) string {
return strconv.FormatInt(int64(v), 10)
}
// RetryableFunc is a function that takes the invocation index and returns an error as well as a
// boolean-value flag indicating whether or not the error is considered retryable.
type RetryableFunc = func(attempt int) (bool, error)
// makeRetry returns a function that calls retry with the given max attempt and interval values.
func makeRetry(n int, interval time.Duration) func(f RetryableFunc) error {
return func(f RetryableFunc) error {
return retry(f, n, interval)
}
}
// retry will re-invoke the given function until it returns a nil error value, the function returns
// a non-retryable error (as indicated by its boolean return value), or until the maximum number of
// retries have been attempted. All errors encountered will be returned.
func retry(f RetryableFunc, n int, interval time.Duration) (errs error) {
for i := 0; i <= n; i++ {
retry, err := f(i)
errs = errors.CombineErrors(errs, err)
if err == nil || !retry {
break
}
time.Sleep(interval)
}
return errs
}
type progressCallbackReader struct {
reader io.Reader
totalRead int64
progressCallback func(totalRead int64)
}
var debounceInterval = time.Millisecond * 50
// newProgressCallbackReader returns a modified version of the given reader that
// updates the value of a progress bar on each read. If progress is nil or n is
// zero, then the reader is returned unmodified.
//
// Calls to the progress bar update will be debounced so that two updates do not
// occur within 50ms of each other. This is to reduce flicker on the screen for
// massive writes, which make progress more quickly than the screen can redraw.
func newProgressCallbackReader(r io.Reader, readerLen int64, progress output.Progress, barIndex int) io.Reader {
if progress == nil || readerLen == 0 {
return r
}
var lastUpdated time.Time
progressCallback := func(totalRead int64) {
if debounceInterval <= time.Since(lastUpdated) {
// Calculate progress through the reader; do not ever complete
// as we wait for the HTTP request finish the remaining small
// percentage.
p := float64(totalRead) / float64(readerLen)
if p >= 1 {
p = 1 - 10e-3
}
lastUpdated = time.Now()
progress.SetValue(barIndex, p)
}
}
return &progressCallbackReader{reader: r, progressCallback: progressCallback}
}
func (r *progressCallbackReader) Read(p []byte) (int, error) {
n, err := r.reader.Read(p)
r.totalRead += int64(n)
r.progressCallback(r.totalRead)
return n, err
}
// compressReaderToDisk compresses and writes the content of the given reader to a temporary
// file and returns the file's path. If the given progress object is non-nil, then the progress's
// first bar will be updated with the percentage of bytes read on each read.
func compressReaderToDisk(r io.Reader, readerLen int64, progress output.Progress) (filename string, err error) {
compressedFile, err := os.CreateTemp("", "")
if err != nil {
return "", err
}
defer func() {
if closeErr := compressedFile.Close(); err != nil {
err = errors.Append(err, closeErr)
}
}()
gzipWriter := gzip.NewWriter(compressedFile)
defer func() {
if closeErr := gzipWriter.Close(); err != nil {
err = errors.Append(err, closeErr)
}
}()
if progress != nil {
r = newProgressCallbackReader(r, readerLen, progress, 0)
}
if _, err := io.Copy(gzipWriter, r); err != nil {
return "", nil
}
return compressedFile.Name(), nil
}