-
Notifications
You must be signed in to change notification settings - Fork 103
Feat: Don't retry function upload on 400 or 422 status #478
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 14 commits
a12f834
6eaf7b2
2f75b17
7d7c6a7
03e50c8
279e58b
7d176fb
de2b8c1
9b326af
6248a62
49e8d71
0116127
047bf94
2192c51
1367935
75e7e49
107dbe6
0276651
ff30a75
10097e7
25f5b63
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 | ||||
---|---|---|---|---|---|---|
|
@@ -83,7 +83,8 @@ type DeployOptions struct { | |||||
BuildDir string | ||||||
LargeMediaEnabled bool | ||||||
|
||||||
IsDraft bool | ||||||
IsDraft bool | ||||||
SkipRetry bool | ||||||
|
||||||
Title string | ||||||
Branch string | ||||||
|
@@ -344,12 +345,14 @@ func (n *Netlify) DoDeploy(ctx context.Context, options *DeployOptions, deploy * | |||||
return deploy, nil | ||||||
} | ||||||
|
||||||
if err := n.uploadFiles(ctx, deploy, options.files, options.Observer, fileUpload, options.UploadTimeout); err != nil { | ||||||
skipRetry := options.SkipRetry | ||||||
|
||||||
if err := n.uploadFiles(ctx, deploy, options.files, options.Observer, fileUpload, options.UploadTimeout, skipRetry); err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
if options.functions != nil { | ||||||
if err := n.uploadFiles(ctx, deploy, options.functions, options.Observer, functionUpload, options.UploadTimeout); err != nil { | ||||||
if err := n.uploadFiles(ctx, deploy, options.functions, options.Observer, functionUpload, options.UploadTimeout, skipRetry); err != nil { | ||||||
return nil, err | ||||||
} | ||||||
} | ||||||
|
@@ -401,8 +404,9 @@ func (n *Netlify) WaitUntilDeployLive(ctx context.Context, d *models.Deploy) (*m | |||||
return n.waitForState(ctx, d, "ready") | ||||||
} | ||||||
|
||||||
func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *deployFiles, observer DeployObserver, t uploadType, timeout time.Duration) error { | ||||||
func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *deployFiles, observer DeployObserver, t uploadType, timeout time.Duration, skipRetry bool) error { | ||||||
sharedErr := &uploadError{err: nil, mutex: &sync.Mutex{}} | ||||||
permanentErr := &backoff.PermanentError{Err: nil} | ||||||
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. It's a bit unclear for me why we instantiate as far as I know 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. Initially we weren't sure how to use PermanentError, but your other comment pointed us in the right direction I think |
||||||
sem := make(chan int, n.uploadLimit) | ||||||
wg := &sync.WaitGroup{} | ||||||
|
||||||
|
@@ -431,7 +435,7 @@ func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *depl | |||||
select { | ||||||
case sem <- 1: | ||||||
wg.Add(1) | ||||||
go n.uploadFile(ctx, d, file, observer, t, timeout, wg, sem, sharedErr) | ||||||
go n.uploadFile(ctx, d, file, observer, t, timeout, wg, sem, sharedErr, permanentErr, skipRetry) | ||||||
case <-ctx.Done(): | ||||||
log.Info("Context terminated, aborting file upload") | ||||||
return errors.Wrap(ctx.Err(), "aborted file upload early") | ||||||
|
@@ -451,7 +455,7 @@ func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *depl | |||||
return sharedErr.err | ||||||
} | ||||||
|
||||||
func (n *Netlify) uploadFile(ctx context.Context, d *models.Deploy, f *FileBundle, c DeployObserver, t uploadType, timeout time.Duration, wg *sync.WaitGroup, sem chan int, sharedErr *uploadError) { | ||||||
func (n *Netlify) uploadFile(ctx context.Context, d *models.Deploy, f *FileBundle, c DeployObserver, t uploadType, timeout time.Duration, wg *sync.WaitGroup, sem chan int, sharedErr *uploadError, permanentErr *backoff.PermanentError, skipRetry bool) { | ||||||
defer func() { | ||||||
wg.Done() | ||||||
<-sem | ||||||
|
@@ -538,10 +542,16 @@ func (n *Netlify) uploadFile(ctx context.Context, d *models.Deploy, f *FileBundl | |||||
context.GetLogger(ctx).WithError(operationError).Errorf("Failed to upload file %v", f.Name) | ||||||
apiErr, ok := operationError.(apierrors.Error) | ||||||
|
||||||
if ok && apiErr.Code() == 401 { | ||||||
sharedErr.mutex.Lock() | ||||||
sharedErr.err = operationError | ||||||
sharedErr.mutex.Unlock() | ||||||
if ok { | ||||||
if apiErr.Code() == 401 { | ||||||
sharedErr.mutex.Lock() | ||||||
sharedErr.err = operationError | ||||||
sharedErr.mutex.Unlock() | ||||||
} | ||||||
|
||||||
if skipRetry && (apiErr.Code() == 400 || apiErr.Code() == 422) { | ||||||
operationError = permanentErr | ||||||
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 might be wrong, but I think we should wrap the existing error in a
Suggested change
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. Ah, I think that does make sense. And then when |
||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -287,7 +287,7 @@ func TestUploadFiles_Cancelation(t *testing.T) { | |||||||||
for _, bundle := range files.Files { | ||||||||||
d.Required = append(d.Required, bundle.Sum) | ||||||||||
} | ||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute) | ||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute, false) | ||||||||||
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. Wondering if we could add a test here which exercises the new code paths we introduced (i.e. checking we don't retry 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. @JGAntunes do you have any pointers for the testing? I seem to be a bit stuck here. We are also having trouble running the test suite locally, which is slowing this down a bit 😬 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. about running the test suite locally, if it seems like nothing is running it's probably because of the retries loop. try running the tests as:
And you should get some output (hopefully) 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. Thanks for the tip @4xposed!! We got the tests running and can see the output which has been very helpful :D |
||||||||||
require.ErrorIs(t, err, gocontext.Canceled) | ||||||||||
} | ||||||||||
|
||||||||||
|
@@ -317,9 +317,38 @@ func TestUploadFiles_Errors(t *testing.T) { | |||||||||
for _, bundle := range files.Files { | ||||||||||
d.Required = append(d.Required, bundle.Sum) | ||||||||||
} | ||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute) | ||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute, false) | ||||||||||
require.Equal(t, err.Error(), "[PUT /deploys/{deploy_id}/files/{path}][500] uploadDeployFile default &{Code:0 Message:}") | ||||||||||
} | ||||||||||
func TestUploadFiles400Errors(t *testing.T) { | ||||||||||
ctx := gocontext.Background() | ||||||||||
|
||||||||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { | ||||||||||
jenae-janzen marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
rw.WriteHeader(http.StatusUnprocessableEntity) | ||||||||||
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 think to properly assert the error message in the
Suggested change
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. This is super helpful and definitely makes sense! -- looking at other test cases I see this is done similarly. However, when we set So then the apiError isn't set and then our test skips over the code we're trying to test and keeps retrying 🙈 Looking at other examples in other tests, it seems like it works to set the response this way when calling We've been trying to debug this by writing the body different ways, trying to marshal the response etc, but haven't had any success yet 🤔 Is there maybe something silly we're missing? 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. This is where the error is being thrown from: https://github.com/netlify/open-api/blob/master/go/plumbing/operations/operations_client.go#L4131 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 think It might be the order on which those are set, mockserver is a bit quirky at times. try if setting the headers in this order helps: rw.Header().Set("Content-Type", "application/json; charset=utf-8")
rw.WriteHeader(http.StatusUnprocessableEntity)
rw.Write([]byte(`{"message": "Unprocessable Entity", "code": 422 }`)) 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. 👋 @4xposed thanks so much for the tip! That worked and now as far as I can tell, the server is responding as expected. However, now we're hitting a different problem and struggling to debug it. We now receive the error with the correct code and message, but when we get to this line
In my debugger, I can access the status code by calling either So since Are we missing something here? Is there a better way to test this (perhaps non-locally?) 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. @4xposed Thanks, I really appreciate the offer! I'm in Europe now 🎉 Testing manually using the debugger, it looks like everything's working as expected, thanks so much for the fix. The one last thing I'm trying to do to wrap up this PR is to fix my tests:
doesn't work because the error's now wrapped by the 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. Pairing might not be necessary since most of this seems to be working, but I'd appreciate you taking another look when you have the time 🙂 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. we could call I don't think it's strictly necessary, but it would be nice to at least have some way that we have the right error and not any error, if that makes sense. What do you think? 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'll give it a shot! I do agree it would be nice to have the right error if possible 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. Okay, I used |
||||||||||
})) | ||||||||||
defer server.Close() | ||||||||||
|
||||||||||
hu, _ := url.Parse(server.URL) | ||||||||||
tr := apiClient.NewWithClient(hu.Host, "/api/v1", []string{"http"}, http.DefaultClient) | ||||||||||
client := NewRetryable(tr, strfmt.Default, 1) | ||||||||||
client.uploadLimit = 1 | ||||||||||
ctx = context.WithAuthInfo(ctx, apiClient.BearerToken("bad")) | ||||||||||
|
||||||||||
// Create some files to deploy | ||||||||||
dir, err := ioutil.TempDir("", "deploy") | ||||||||||
require.NoError(t, err) | ||||||||||
defer os.RemoveAll(dir) | ||||||||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "foo.html"), []byte("Hello"), 0644)) | ||||||||||
|
||||||||||
files, err := walk(dir, nil, false, false) | ||||||||||
require.NoError(t, err) | ||||||||||
d := &models.Deploy{} | ||||||||||
for _, bundle := range files.Files { | ||||||||||
d.Required = append(d.Required, bundle.Sum) | ||||||||||
} | ||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute, true) | ||||||||||
require.Equal(t, err.Error(), "[PUT /deploys/{deploy_id}/files/{path}][401] uploadDeployFile default &{Code:401 Message: Unauthorized}") | ||||||||||
jenae-janzen marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
} | ||||||||||
|
||||||||||
func TestUploadFiles_SkipEqualFiles(t *testing.T) { | ||||||||||
ctx := gocontext.Background() | ||||||||||
|
@@ -377,11 +406,11 @@ func TestUploadFiles_SkipEqualFiles(t *testing.T) { | |||||||||
d.Required = []string{files.Sums["a.html"]} | ||||||||||
d.RequiredFunctions = []string{functions.Sums["a"]} | ||||||||||
|
||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute) | ||||||||||
err = client.uploadFiles(ctx, d, files, nil, fileUpload, time.Minute, false) | ||||||||||
require.NoError(t, err) | ||||||||||
assert.Equal(t, 1, serverRequests) | ||||||||||
|
||||||||||
err = client.uploadFiles(ctx, d, functions, nil, functionUpload, time.Minute) | ||||||||||
err = client.uploadFiles(ctx, d, functions, nil, functionUpload, time.Minute, false) | ||||||||||
require.NoError(t, err) | ||||||||||
assert.Equal(t, 2, serverRequests) | ||||||||||
} | ||||||||||
|
@@ -437,7 +466,7 @@ func TestUploadFunctions_RetryCountHeader(t *testing.T) { | |||||||||
d.RequiredFunctions = append(d.RequiredFunctions, bundle.Sum) | ||||||||||
} | ||||||||||
|
||||||||||
require.NoError(t, client.uploadFiles(apiCtx, d, files, nil, functionUpload, time.Minute)) | ||||||||||
require.NoError(t, client.uploadFiles(apiCtx, d, files, nil, functionUpload, time.Minute, false)) | ||||||||||
} | ||||||||||
|
||||||||||
func TestBundle(t *testing.T) { | ||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.