mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-04-29 19:10:51 -05:00
fix(migration): add retry to migration request helper
Resolves https://github.com/go-vikunja/vikunja/issues/1788
This commit is contained in:
@@ -19,9 +19,13 @@ package migration
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DownloadFile downloads a file and returns its contents
|
||||
@@ -61,17 +65,48 @@ func DoPost(url string, form url.Values) (resp *http.Response, err error) {
|
||||
|
||||
// DoPostWithHeaders does an api request and allows to pass in arbitrary headers
|
||||
func DoPostWithHeaders(url string, form url.Values, headers map[string]string) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
const maxRetries = 3
|
||||
const baseDelay = 100 * time.Millisecond
|
||||
|
||||
hc := http.Client{}
|
||||
return hc.Do(req)
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
resp, err = hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Don't retry on non-5xx status codes
|
||||
if resp.StatusCode < 500 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Don't retry on last attempt
|
||||
if attempt == maxRetries-1 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Close the body before retrying
|
||||
resp.Body.Close()
|
||||
|
||||
// Exponential backoff with jitter
|
||||
delay := baseDelay * time.Duration(math.Pow(2, float64(attempt)))
|
||||
maxJitter := int64(delay / 2)
|
||||
jitterBig, _ := rand.Int(rand.Reader, big.NewInt(maxJitter))
|
||||
jitter := time.Duration(jitterBig.Int64())
|
||||
time.Sleep(delay + jitter)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
98
pkg/modules/migration/helpers_test.go
Normal file
98
pkg/modules/migration/helpers_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDoPostWithHeaders_RetriesOn500(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
count := attempts.Add(1)
|
||||
if count < 3 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
form := url.Values{"key": {"value"}}
|
||||
resp, err := DoPostWithHeaders(server.URL, form, map[string]string{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if attempts.Load() != 3 {
|
||||
t.Errorf("expected 3 attempts, got %d", attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoPostWithHeaders_GivesUpAfter3Retries(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
attempts.Add(1)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
form := url.Values{"key": {"value"}}
|
||||
resp, err := DoPostWithHeaders(server.URL, form, map[string]string{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", resp.StatusCode)
|
||||
}
|
||||
if attempts.Load() != 3 {
|
||||
t.Errorf("expected 3 attempts, got %d", attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoPostWithHeaders_DoesNotRetryOn4xx(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
attempts.Add(1)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
form := url.Values{"key": {"value"}}
|
||||
resp, err := DoPostWithHeaders(server.URL, form, map[string]string{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", resp.StatusCode)
|
||||
}
|
||||
if attempts.Load() != 1 {
|
||||
t.Errorf("expected 1 attempt (no retries on 4xx), got %d", attempts.Load())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user