From 045be3905b38341ecb2cc6db66bcd8d8f25bb852 Mon Sep 17 00:00:00 2001 From: Gilbert Chen Date: Thu, 13 Jun 2019 22:43:07 -0400 Subject: [PATCH] Better handling of B2 authorization failures This commit fixed 2 issues wrt Backblaze B2 authorization: * every thread may call b2_authorize_account at the same time when there are 401 errors * if B2 has a login outage, then all threads will call b2_authorize_account repeatedly without delay A simple solution is to limit one b2_authorize_account call to once every 30 second regardless of how many threads there are. If the call to b2_authorize_account is not allowed, the random exponential backoff will be performed. --- src/duplicacy_b2client.go | 25 +++++++++++++++++++------ src/duplicacy_b2client_test.go | 2 +- src/duplicacy_b2storage.go | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/duplicacy_b2client.go b/src/duplicacy_b2client.go index 7f8f92e..9d82622 100644 --- a/src/duplicacy_b2client.go +++ b/src/duplicacy_b2client.go @@ -62,6 +62,8 @@ type B2Client struct { Threads int MaximumRetries int TestMode bool + + LastAuthorizationTime int64 } // URL encode the given path but keep the slashes intact @@ -253,8 +255,12 @@ func (client *B2Client) call(threadIndex int, requestURL string, method string, if requestURL == B2AuthorizationURL { return nil, nil, 0, fmt.Errorf("Authorization failure") } - client.AuthorizeAccount(threadIndex) - continue + + // Attempt authorization again. If authorization is actually not done, run the random backoff + _, allowed := client.AuthorizeAccount(threadIndex) + if allowed { + continue + } } else if response.StatusCode == 403 { if !client.TestMode { return nil, nil, 0, fmt.Errorf("B2 cap exceeded") @@ -291,13 +297,18 @@ type B2AuthorizeAccountOutput struct { DownloadURL string } -func (client *B2Client) AuthorizeAccount(threadIndex int) (err error) { +func (client *B2Client) AuthorizeAccount(threadIndex int) (err error, allowed bool) { client.Lock.Lock() defer client.Lock.Unlock() + // Don't authorize if the previous one was done less than 30 seconds ago + if client.LastAuthorizationTime != 0 && client.LastAuthorizationTime > time.Now().Unix() - 30 { + return nil, false + } + readCloser, _, _, err := client.call(threadIndex, B2AuthorizationURL, http.MethodPost, nil, make(map[string]string)) if err != nil { - return err + return err, true } defer readCloser.Close() @@ -305,7 +316,7 @@ func (client *B2Client) AuthorizeAccount(threadIndex int) (err error) { output := &B2AuthorizeAccountOutput{} if err = json.NewDecoder(readCloser).Decode(&output); err != nil { - return err + return err, true } // The account id may be different from the application key id so we're getting the account id from the returned @@ -317,7 +328,9 @@ func (client *B2Client) AuthorizeAccount(threadIndex int) (err error) { client.DownloadURL = output.DownloadURL client.IsAuthorized = true - return nil + client.LastAuthorizationTime = time.Now().Unix() + + return nil, true } type ListBucketOutput struct { diff --git a/src/duplicacy_b2client_test.go b/src/duplicacy_b2client_test.go index 9c5ad9c..ebcc5df 100644 --- a/src/duplicacy_b2client_test.go +++ b/src/duplicacy_b2client_test.go @@ -50,7 +50,7 @@ func TestB2Client(t *testing.T) { b2Client.TestMode = true - err := b2Client.AuthorizeAccount(0) + err, _ := b2Client.AuthorizeAccount(0) if err != nil { t.Errorf("Failed to authorize the b2 account: %v", err) return diff --git a/src/duplicacy_b2storage.go b/src/duplicacy_b2storage.go index f54f992..08af6a6 100644 --- a/src/duplicacy_b2storage.go +++ b/src/duplicacy_b2storage.go @@ -19,7 +19,7 @@ func CreateB2Storage(accountID string, applicationKey string, bucket string, sto client := NewB2Client(accountID, applicationKey, storageDir, threads) - err = client.AuthorizeAccount(0) + err, _ = client.AuthorizeAccount(0) if err != nil { return nil, err }