diff --git a/fetcher/account.go b/fetcher/account.go index 3d60e1b6..d7a7470b 100644 --- a/fetcher/account.go +++ b/fetcher/account.go @@ -16,7 +16,6 @@ package fetcher import ( "context" - "errors" "fmt" "github.com/coinbase/rosetta-sdk-go/asserter" @@ -71,7 +70,7 @@ func (f *Fetcher) AccountBalanceRetry( f.maxRetries, ) - for ctx.Err() == nil { + for { responseBlock, balances, metadata, err := f.AccountBalance( ctx, network, @@ -82,10 +81,22 @@ func (f *Fetcher) AccountBalanceRetry( return responseBlock, balances, metadata, nil } - if !tryAgain(fmt.Sprintf("account %s", account.Address), backoffRetries, err) { + if ctx.Err() != nil { + return nil, nil, nil, ctx.Err() + } + + if !tryAgain( + fmt.Sprintf("account %s", types.PrettyPrintStruct(account)), + backoffRetries, + err, + ) { break } } - return nil, nil, nil, errors.New("exhausted retries for account") + return nil, nil, nil, fmt.Errorf( + "%w: unable to fetch account %s", + ErrExhaustedRetries, + types.PrettyPrintStruct(account), + ) } diff --git a/fetcher/account_test.go b/fetcher/account_test.go new file mode 100644 index 00000000..e3b97402 --- /dev/null +++ b/fetcher/account_test.go @@ -0,0 +1,164 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fetcher + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/stretchr/testify/assert" +) + +var ( + basicNetwork = &types.NetworkIdentifier{ + Blockchain: "blockchain", + Network: "network", + } + + basicAccount = &types.AccountIdentifier{ + Address: "address", + } + + basicBlock = &types.BlockIdentifier{ + Index: 10, + Hash: "block 10", + } + + basicAmounts = []*types.Amount{ + { + Value: "1000", + Currency: &types.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + }, + } +) + +func TestAccountBalanceRetry(t *testing.T) { + var tests = map[string]struct { + network *types.NetworkIdentifier + account *types.AccountIdentifier + + errorsBeforeSuccess int + expectedBlock *types.BlockIdentifier + expectedAmounts []*types.Amount + expectedError error + + fetcherMaxRetries uint64 + shouldCancel bool + }{ + "no failures": { + network: basicNetwork, + account: basicAccount, + expectedBlock: basicBlock, + expectedAmounts: basicAmounts, + fetcherMaxRetries: 5, + }, + "retry failures": { + network: basicNetwork, + account: basicAccount, + errorsBeforeSuccess: 2, + expectedBlock: basicBlock, + expectedAmounts: basicAmounts, + fetcherMaxRetries: 5, + }, + "exhausted retries": { + network: basicNetwork, + account: basicAccount, + errorsBeforeSuccess: 2, + expectedError: ErrExhaustedRetries, + fetcherMaxRetries: 1, + }, + "cancel context": { + network: basicNetwork, + account: basicAccount, + errorsBeforeSuccess: 6, + expectedError: context.Canceled, + fetcherMaxRetries: 5, + shouldCancel: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var ( + tries = 0 + assert = assert.New(t) + ctx, cancel = context.WithCancel(context.Background()) + endpoint = "/account/balance" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("POST", r.Method) + assert.Equal(endpoint, r.URL.RequestURI()) + + expected := &types.AccountBalanceRequest{ + NetworkIdentifier: test.network, + AccountIdentifier: test.account, + } + var accountRequest *types.AccountBalanceRequest + assert.NoError(json.NewDecoder(r.Body).Decode(&accountRequest)) + assert.Equal(expected, accountRequest) + + if test.shouldCancel { + cancel() + } + + if tries < test.errorsBeforeSuccess { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "{}") + tries++ + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, types.PrettyPrintStruct( + &types.AccountBalanceResponse{ + BlockIdentifier: test.expectedBlock, + Balances: test.expectedAmounts, + }, + )) + })) + + defer ts.Close() + + f := New( + ts.URL, + WithRetryElapsedTime(5*time.Second), + WithMaxRetries(test.fetcherMaxRetries), + ) + block, amounts, metadata, err := f.AccountBalanceRetry( + ctx, + test.network, + test.account, + nil, + ) + assert.Equal(test.expectedBlock, block) + assert.Equal(test.expectedAmounts, amounts) + assert.Nil(metadata) + assert.True(errors.Is(err, test.expectedError)) + }) + } +} diff --git a/fetcher/block.go b/fetcher/block.go index ca2281ce..35bf9ca9 100644 --- a/fetcher/block.go +++ b/fetcher/block.go @@ -16,7 +16,6 @@ package fetcher import ( "context" - "errors" "fmt" "github.com/coinbase/rosetta-sdk-go/asserter" @@ -201,7 +200,7 @@ func (f *Fetcher) BlockRetry( f.maxRetries, ) - for ctx.Err() == nil { + for { block, err := f.Block( ctx, network, @@ -211,6 +210,10 @@ func (f *Fetcher) BlockRetry( return block, nil } + if ctx.Err() != nil { + return nil, ctx.Err() + } + var blockFetchErr string if blockIdentifier.Index != nil { blockFetchErr = fmt.Sprintf("block %d", *blockIdentifier.Index) @@ -223,7 +226,11 @@ func (f *Fetcher) BlockRetry( } } - return nil, errors.New("exhausted retries for block") + return nil, fmt.Errorf( + "%w: unable to fetch block %s", + ErrExhaustedRetries, + types.PrettyPrintStruct(blockIdentifier), + ) } // addIndicies appends a range of indicies (from diff --git a/fetcher/block_test.go b/fetcher/block_test.go new file mode 100644 index 00000000..96ea4b67 --- /dev/null +++ b/fetcher/block_test.go @@ -0,0 +1,155 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fetcher + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/stretchr/testify/assert" +) + +var ( + basicFullBlock = &types.Block{ + BlockIdentifier: basicBlock, + ParentBlockIdentifier: &types.BlockIdentifier{ + Index: 9, + Hash: "block 9", + }, + Timestamp: 1582833600000, + } +) + +func TestBlockRetry(t *testing.T) { + var tests = map[string]struct { + network *types.NetworkIdentifier + block *types.BlockIdentifier + + errorsBeforeSuccess int + expectedBlock *types.Block + expectedError error + + fetcherMaxRetries uint64 + shouldCancel bool + }{ + "no failures": { + network: basicNetwork, + block: basicBlock, + expectedBlock: basicFullBlock, + fetcherMaxRetries: 5, + }, + "retry failures": { + network: basicNetwork, + block: basicBlock, + errorsBeforeSuccess: 2, + expectedBlock: basicFullBlock, + fetcherMaxRetries: 5, + }, + "exhausted retries": { + network: basicNetwork, + block: basicBlock, + errorsBeforeSuccess: 2, + expectedError: ErrExhaustedRetries, + fetcherMaxRetries: 1, + }, + "cancel context": { + network: basicNetwork, + block: basicBlock, + errorsBeforeSuccess: 6, + expectedError: context.Canceled, + fetcherMaxRetries: 5, + shouldCancel: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var ( + tries = 0 + assert = assert.New(t) + ctx, cancel = context.WithCancel(context.Background()) + endpoint = "/block" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("POST", r.Method) + assert.Equal(endpoint, r.URL.RequestURI()) + + expected := &types.BlockRequest{ + NetworkIdentifier: test.network, + BlockIdentifier: types.ConstructPartialBlockIdentifier(test.block), + } + var blockRequest *types.BlockRequest + assert.NoError(json.NewDecoder(r.Body).Decode(&blockRequest)) + assert.Equal(expected, blockRequest) + + if test.shouldCancel { + cancel() + } + + if tries < test.errorsBeforeSuccess { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "{}") + tries++ + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, types.PrettyPrintStruct( + &types.BlockResponse{ + Block: test.expectedBlock, + }, + )) + })) + + defer ts.Close() + a, err := asserter.NewClientWithOptions( + basicNetwork, + &types.BlockIdentifier{ + Index: 0, + Hash: "block 0", + }, + basicNetworkOptions.Allow.OperationTypes, + basicNetworkOptions.Allow.OperationStatuses, + nil, + ) + assert.NoError(err) + + f := New( + ts.URL, + WithRetryElapsedTime(5*time.Second), + WithMaxRetries(test.fetcherMaxRetries), + WithAsserter(a), + ) + block, err := f.BlockRetry( + ctx, + test.network, + types.ConstructPartialBlockIdentifier(test.block), + ) + assert.Equal(test.expectedBlock, block) + assert.True(errors.Is(err, test.expectedError)) + }) + } +} diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index c08354ac..b0b4abc8 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -54,6 +54,12 @@ const ( DefaultUserAgent = "rosetta-sdk-go" ) +var ( + // ErrExhaustedRetries is returned when a fetch with retries + // fails because it was attempted too many times. + ErrExhaustedRetries = errors.New("retries exhausted") +) + // Fetcher contains all logic to communicate with a Rosetta Server. type Fetcher struct { // Asserter is a public variable because diff --git a/fetcher/network.go b/fetcher/network.go index 9b94a332..1a26da31 100644 --- a/fetcher/network.go +++ b/fetcher/network.go @@ -16,7 +16,7 @@ package fetcher import ( "context" - "errors" + "fmt" "github.com/coinbase/rosetta-sdk-go/asserter" @@ -60,7 +60,7 @@ func (f *Fetcher) NetworkStatusRetry( f.maxRetries, ) - for ctx.Err() == nil { + for { networkStatus, err := f.NetworkStatus( ctx, network, @@ -70,12 +70,24 @@ func (f *Fetcher) NetworkStatusRetry( return networkStatus, nil } - if !tryAgain("NetworkStatus", backoffRetries, err) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + if !tryAgain( + fmt.Sprintf("network status %s", types.PrettyPrintStruct(network)), + backoffRetries, + err, + ) { break } } - return nil, errors.New("exhausted retries for NetworkStatus") + return nil, fmt.Errorf( + "%w: unable to fetch network status %s", + ErrExhaustedRetries, + types.PrettyPrintStruct(network), + ) } // NetworkList returns the validated response @@ -112,7 +124,7 @@ func (f *Fetcher) NetworkListRetry( f.maxRetries, ) - for ctx.Err() == nil { + for { networkList, err := f.NetworkList( ctx, metadata, @@ -121,12 +133,19 @@ func (f *Fetcher) NetworkListRetry( return networkList, nil } + if ctx.Err() != nil { + return nil, ctx.Err() + } + if !tryAgain("NetworkList", backoffRetries, err) { break } } - return nil, errors.New("exhausted retries for NetworkList") + return nil, fmt.Errorf( + "%w: unable to fetch network list", + ErrExhaustedRetries, + ) } // NetworkOptions returns the validated response @@ -166,7 +185,7 @@ func (f *Fetcher) NetworkOptionsRetry( f.maxRetries, ) - for ctx.Err() == nil { + for { networkOptions, err := f.NetworkOptions( ctx, network, @@ -176,10 +195,22 @@ func (f *Fetcher) NetworkOptionsRetry( return networkOptions, nil } - if !tryAgain("NetworkOptions", backoffRetries, err) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + if !tryAgain( + fmt.Sprintf("network options %s", types.PrettyPrintStruct(network)), + backoffRetries, + err, + ) { break } } - return nil, errors.New("exhausted retries for NetworkOptions") + return nil, fmt.Errorf( + "%w: unable to fetch network options %s", + ErrExhaustedRetries, + types.PrettyPrintStruct(network), + ) } diff --git a/fetcher/network_test.go b/fetcher/network_test.go new file mode 100644 index 00000000..1b17ad97 --- /dev/null +++ b/fetcher/network_test.go @@ -0,0 +1,333 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fetcher + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/stretchr/testify/assert" +) + +var ( + basicNetworkStatus = &types.NetworkStatusResponse{ + CurrentBlockIdentifier: basicBlock, + CurrentBlockTimestamp: 1582833600000, + GenesisBlockIdentifier: &types.BlockIdentifier{ + Index: 0, + Hash: "block 0", + }, + } + + basicNetworkList = &types.NetworkListResponse{ + NetworkIdentifiers: []*types.NetworkIdentifier{ + basicNetwork, + }, + } + + basicNetworkOptions = &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: "1.3.1", + NodeVersion: "0.0.1", + }, + Allow: &types.Allow{ + OperationStatuses: []*types.OperationStatus{ + { + Status: "SUCCESS", + Successful: true, + }, + }, + OperationTypes: []string{"transfer"}, + }, + } +) + +func TestNetworkStatusRetry(t *testing.T) { + var tests = map[string]struct { + network *types.NetworkIdentifier + + errorsBeforeSuccess int + expectedStatus *types.NetworkStatusResponse + expectedError error + + fetcherMaxRetries uint64 + shouldCancel bool + }{ + "no failures": { + network: basicNetwork, + expectedStatus: basicNetworkStatus, + fetcherMaxRetries: 5, + }, + "retry failures": { + network: basicNetwork, + errorsBeforeSuccess: 2, + expectedStatus: basicNetworkStatus, + fetcherMaxRetries: 5, + }, + "exhausted retries": { + network: basicNetwork, + errorsBeforeSuccess: 2, + expectedError: ErrExhaustedRetries, + fetcherMaxRetries: 1, + }, + "cancel context": { + network: basicNetwork, + errorsBeforeSuccess: 6, + expectedError: context.Canceled, + fetcherMaxRetries: 5, + shouldCancel: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var ( + tries = 0 + assert = assert.New(t) + ctx, cancel = context.WithCancel(context.Background()) + endpoint = "/network/status" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("POST", r.Method) + assert.Equal(endpoint, r.URL.RequestURI()) + + expected := &types.NetworkRequest{ + NetworkIdentifier: test.network, + } + var networkRequest *types.NetworkRequest + assert.NoError(json.NewDecoder(r.Body).Decode(&networkRequest)) + assert.Equal(expected, networkRequest) + + if test.shouldCancel { + cancel() + } + + if tries < test.errorsBeforeSuccess { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "{}") + tries++ + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, types.PrettyPrintStruct(test.expectedStatus)) + })) + + defer ts.Close() + + f := New( + ts.URL, + WithRetryElapsedTime(5*time.Second), + WithMaxRetries(test.fetcherMaxRetries), + ) + status, err := f.NetworkStatusRetry( + ctx, + test.network, + nil, + ) + assert.Equal(test.expectedStatus, status) + assert.True(errors.Is(err, test.expectedError)) + }) + } +} + +func TestNetworkListRetry(t *testing.T) { + var tests = map[string]struct { + network *types.NetworkIdentifier + + errorsBeforeSuccess int + expectedList *types.NetworkListResponse + expectedError error + + fetcherMaxRetries uint64 + shouldCancel bool + }{ + "no failures": { + network: basicNetwork, + expectedList: basicNetworkList, + fetcherMaxRetries: 5, + }, + "retry failures": { + network: basicNetwork, + errorsBeforeSuccess: 2, + expectedList: basicNetworkList, + fetcherMaxRetries: 5, + }, + "exhausted retries": { + network: basicNetwork, + errorsBeforeSuccess: 2, + expectedError: ErrExhaustedRetries, + fetcherMaxRetries: 1, + }, + "cancel context": { + network: basicNetwork, + errorsBeforeSuccess: 6, + expectedError: context.Canceled, + fetcherMaxRetries: 5, + shouldCancel: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var ( + tries = 0 + assert = assert.New(t) + ctx, cancel = context.WithCancel(context.Background()) + endpoint = "/network/list" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("POST", r.Method) + assert.Equal(endpoint, r.URL.RequestURI()) + + expected := &types.MetadataRequest{} + var metadataRequest *types.MetadataRequest + assert.NoError(json.NewDecoder(r.Body).Decode(&metadataRequest)) + assert.Equal(expected, metadataRequest) + + if test.shouldCancel { + cancel() + } + + if tries < test.errorsBeforeSuccess { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "{}") + tries++ + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, types.PrettyPrintStruct(test.expectedList)) + })) + + defer ts.Close() + + f := New( + ts.URL, + WithRetryElapsedTime(5*time.Second), + WithMaxRetries(test.fetcherMaxRetries), + ) + list, err := f.NetworkListRetry( + ctx, + nil, + ) + assert.Equal(test.expectedList, list) + assert.True(errors.Is(err, test.expectedError)) + }) + } +} + +func TestNetworkOptionsRetry(t *testing.T) { + var tests = map[string]struct { + network *types.NetworkIdentifier + + errorsBeforeSuccess int + expectedOptions *types.NetworkOptionsResponse + expectedError error + + fetcherMaxRetries uint64 + shouldCancel bool + }{ + "no failures": { + network: basicNetwork, + expectedOptions: basicNetworkOptions, + fetcherMaxRetries: 5, + }, + "retry failures": { + network: basicNetwork, + errorsBeforeSuccess: 2, + expectedOptions: basicNetworkOptions, + fetcherMaxRetries: 5, + }, + "exhausted retries": { + network: basicNetwork, + errorsBeforeSuccess: 2, + expectedError: ErrExhaustedRetries, + fetcherMaxRetries: 1, + }, + "cancel context": { + network: basicNetwork, + errorsBeforeSuccess: 6, + expectedError: context.Canceled, + fetcherMaxRetries: 5, + shouldCancel: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var ( + tries = 0 + assert = assert.New(t) + ctx, cancel = context.WithCancel(context.Background()) + endpoint = "/network/options" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("POST", r.Method) + assert.Equal(endpoint, r.URL.RequestURI()) + + expected := &types.NetworkRequest{ + NetworkIdentifier: test.network, + } + var networkRequest *types.NetworkRequest + assert.NoError(json.NewDecoder(r.Body).Decode(&networkRequest)) + assert.Equal(expected, networkRequest) + + if test.shouldCancel { + cancel() + } + + if tries < test.errorsBeforeSuccess { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "{}") + tries++ + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, types.PrettyPrintStruct(test.expectedOptions)) + })) + + defer ts.Close() + + f := New( + ts.URL, + WithRetryElapsedTime(5*time.Second), + WithMaxRetries(test.fetcherMaxRetries), + ) + options, err := f.NetworkOptionsRetry( + ctx, + test.network, + nil, + ) + assert.Equal(test.expectedOptions, options) + assert.True(errors.Is(err, test.expectedError)) + }) + } +} diff --git a/fetcher/utils.go b/fetcher/utils.go index bd7360e4..40518d6b 100644 --- a/fetcher/utils.go +++ b/fetcher/utils.go @@ -16,6 +16,7 @@ package fetcher import ( "log" + "strings" "time" "github.com/cenkalti/backoff" @@ -35,6 +36,7 @@ func backoffRetries( // tryAgain handles a backoff and prints error messages depending // on the fetchMsg. func tryAgain(fetchMsg string, thisBackoff backoff.BackOff, err error) bool { + fetchMsg = strings.Replace(fetchMsg, "\n", "", -1) log.Printf("%s fetch error: %s\n", fetchMsg, err.Error()) nextBackoff := thisBackoff.NextBackOff() diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 42a63209..f23b0759 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -416,7 +416,11 @@ func (r *Reconciler) accountReconciliation( Account: account, Currency: currency, } - for ctx.Err() == nil { + for { + if ctx.Err() != nil { + return ctx.Err() + } + // If don't have previous balance because stateless, check diff on block // instead of comparing entire computed balance difference, cachedBalance, headIndex, err := r.CompareBalance( @@ -580,7 +584,11 @@ func (r *Reconciler) reconcileActiveAccounts( func (r *Reconciler) reconcileInactiveAccounts( ctx context.Context, ) error { - for ctx.Err() == nil { + for { + if ctx.Err() != nil { + return ctx.Err() + } + head, err := r.helper.CurrentBlock(ctx) // When first start syncing, this loop may run before the genesis block is synced. // If this is the case, we should sleep and try again later instead of exiting. @@ -624,8 +632,6 @@ func (r *Reconciler) reconcileInactiveAccounts( time.Sleep(inactiveReconciliationSleep) } } - - return nil } // Reconcile starts the active and inactive Reconciler goroutines.