-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Make LFS http_client parallel within a batch. #32369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4653cad
dfe8978
3a5889c
906c3fd
c78892e
02d55ff
f903f76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ import ( | |
"code.gitea.io/gitea/modules/log" | ||
"code.gitea.io/gitea/modules/proxy" | ||
"code.gitea.io/gitea/modules/setting" | ||
|
||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
// HTTPClient is used to communicate with the LFS server | ||
|
@@ -113,6 +115,7 @@ func (c *HTTPClient) Upload(ctx context.Context, objects []Pointer, callback Upl | |
return c.performOperation(ctx, objects, nil, callback) | ||
} | ||
|
||
// performOperation takes a slice of LFS object pointers, batches them, and performs the upload/download operations concurrently in each batch | ||
func (c *HTTPClient) performOperation(ctx context.Context, objects []Pointer, dc DownloadCallback, uc UploadCallback) error { | ||
if len(objects) == 0 { | ||
return nil | ||
|
@@ -133,71 +136,91 @@ func (c *HTTPClient) performOperation(ctx context.Context, objects []Pointer, dc | |
return fmt.Errorf("TransferAdapter not found: %s", result.Transfer) | ||
} | ||
|
||
errGroup, groupCtx := errgroup.WithContext(ctx) | ||
errGroup.SetLimit(setting.LFSClient.BatchConcurrency) | ||
for _, object := range result.Objects { | ||
if object.Error != nil { | ||
log.Trace("Error on object %v: %v", object.Pointer, object.Error) | ||
if uc != nil { | ||
if _, err := uc(object.Pointer, object.Error); err != nil { | ||
return err | ||
} | ||
} else { | ||
if err := dc(object.Pointer, nil, object.Error); err != nil { | ||
return err | ||
} | ||
} | ||
continue | ||
} | ||
|
||
if uc != nil { | ||
if len(object.Actions) == 0 { | ||
log.Trace("%v already present on server", object.Pointer) | ||
continue | ||
} | ||
errGroup.Go(func() error { | ||
err := performSingleOperation(groupCtx, object, dc, uc, transferAdapter) | ||
return err | ||
}) | ||
} | ||
|
||
link, ok := object.Actions["upload"] | ||
if !ok { | ||
log.Debug("%+v", object) | ||
return errors.New("missing action 'upload'") | ||
} | ||
// only the first error is returned, preserving legacy behavior before concurrency | ||
return errGroup.Wait() | ||
wxiaoguang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
content, err := uc(object.Pointer, nil) | ||
if err != nil { | ||
return err | ||
} | ||
// performSingleOperation performs an LFS upload or download operation on a single object | ||
func performSingleOperation(ctx context.Context, object *ObjectResponse, dc DownloadCallback, uc UploadCallback, transferAdapter TransferAdapter) error { | ||
// the response from an lfs batch api request for this specific object id contained an error | ||
if object.Error != nil { | ||
log.Trace("Error on object %v: %v", object.Pointer, object.Error) | ||
|
||
err = transferAdapter.Upload(ctx, link, object.Pointer, content) | ||
if err != nil { | ||
// this was an 'upload' request inside the batch request | ||
if uc != nil { | ||
if _, err := uc(object.Pointer, object.Error); err != nil { | ||
return err | ||
} | ||
|
||
link, ok = object.Actions["verify"] | ||
if ok { | ||
if err := transferAdapter.Verify(ctx, link, object.Pointer); err != nil { | ||
return err | ||
} | ||
} | ||
} else { | ||
link, ok := object.Actions["download"] | ||
if !ok { | ||
// no actions block in response, try legacy response schema | ||
link, ok = object.Links["download"] | ||
} | ||
if !ok { | ||
log.Debug("%+v", object) | ||
return errors.New("missing action 'download'") | ||
// this was NOT an 'upload' request inside the batch request, meaning it must be a 'download' request | ||
err := dc(object.Pointer, nil, object.Error) | ||
if errors.Is(object.Error, ErrObjectNotExist) { | ||
log.Warn("Ignoring missing upstream LFS object %-v: %v", object.Pointer, err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to point out that this actually reduces the usefulness of this logline. I moved it out of |
||
return nil | ||
} | ||
|
||
content, err := transferAdapter.Download(ctx, link) | ||
if err != nil { | ||
return err | ||
} | ||
// this was a 'download' request which was a legitimate error response from the batch api (not an http/404) | ||
return err | ||
} | ||
} | ||
|
||
if err := dc(object.Pointer, content, nil); err != nil { | ||
// the response from an lfs batch api request contained necessary upload/download fields to act upon | ||
if uc != nil { | ||
if len(object.Actions) == 0 { | ||
log.Trace("%v already present on server", object.Pointer) | ||
return nil | ||
} | ||
|
||
link, ok := object.Actions["upload"] | ||
if !ok { | ||
return errors.New("missing action 'upload'") | ||
} | ||
|
||
content, err := uc(object.Pointer, nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = transferAdapter.Upload(ctx, link, object.Pointer, content) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
link, ok = object.Actions["verify"] | ||
if ok { | ||
if err := transferAdapter.Verify(ctx, link, object.Pointer); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
} else { | ||
link, ok := object.Actions["download"] | ||
if !ok { | ||
// no actions block in response, try legacy response schema | ||
link, ok = object.Links["download"] | ||
} | ||
if !ok { | ||
log.Debug("%+v", object) | ||
return errors.New("missing action 'download'") | ||
} | ||
|
||
content, err := transferAdapter.Download(ctx, link) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := dc(object.Pointer, content, nil); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more thing, I do not think "batch size" means "concurrency limit" in this case. If I understand correctly:
Maybe "batch size" could be 100 or 1000 without causing problem, but "concurrency limit" should be much smaller, eg: 5 or 10. A lot of connections to a LFS server might trigger their rate-limit protection, or be considered as somewhat DoS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm always in favor of making things more configurable, however as an administrator I think most of the time you would want
[lfs_client].BATCH_SIZE
= (the number of concurrent threads for upload/download within a batch). I'm happy to add a new config though, what should we call it and what should its default be?[lfs_client].BATCH_MAX_THREADS
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
[lfs_client].MAX_OPERATION_CONCURRENCY
? (since there is no "thread" concept in Golang)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair, how about
[lfs_client].BATCH_OPERATION_CONCURRENCY
, as I feel there may be other single-threaded operations still in this code that we might want configs for then, and this is specific to lfs client batch operations? I updated this branch with that config, and defaulted it to what[lfs_client].BATCH_SIZE
if unset or <1There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to match the default git-lfs's behavior:
https://github.com/git-lfs/git-lfs/blob/4cca3f8e7d77c5b86cdfe34c2e6bba2126e3d5cd/docs/man/git-lfs-config.5.ronn#L25-L33