diff --git a/mocks/reconciler/helper.go b/mocks/reconciler/helper.go index a75d1739a..fcf72d67c 100644 --- a/mocks/reconciler/helper.go +++ b/mocks/reconciler/helper.go @@ -99,6 +99,20 @@ func (_m *Helper) DatabaseTransaction(ctx context.Context) database.Transaction return r0 } +// ForceInactiveReconciliation provides a mock function with given fields: ctx, account, currency, lastCheck +func (_m *Helper) ForceInactiveReconciliation(ctx context.Context, account *types.AccountIdentifier, currency *types.Currency, lastCheck *types.BlockIdentifier) bool { + ret := _m.Called(ctx, account, currency, lastCheck) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *types.AccountIdentifier, *types.Currency, *types.BlockIdentifier) bool); ok { + r0 = rf(ctx, account, currency, lastCheck) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // IndexAtTip provides a mock function with given fields: ctx, index func (_m *Helper) IndexAtTip(ctx context.Context, index int64) (bool, error) { ret := _m.Called(ctx, index) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index cee3b50a6..da1903a6d 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -834,7 +834,13 @@ func (r *Reconciler) reconcileInactiveAccounts( // nolint:gocognit nextValidIndex = nextAcct.LastCheck.Index + r.inactiveFrequency } - if nextValidIndex <= head.Index { + if nextValidIndex <= head.Index || + r.helper.ForceInactiveReconciliation( + ctx, + nextAcct.Entry.Account, + nextAcct.Entry.Currency, + nextAcct.LastCheck, + ) { r.inactiveQueue = r.inactiveQueue[1:] r.inactiveQueueMutex.Unlock() diff --git a/reconciler/reconciler_test.go b/reconciler/reconciler_test.go index 20ee36cd6..41638ec80 100644 --- a/reconciler/reconciler_test.go +++ b/reconciler/reconciler_test.go @@ -3267,3 +3267,109 @@ func TestPruningRaceConditionInactive(t *testing.T) { mtxn2.AssertExpectations(t) mtxn3.AssertExpectations(t) } + +func TestReconcile_SuccessOnlyInactiveOverride(t *testing.T) { + var ( + block = &types.BlockIdentifier{ + Hash: "block 1", + Index: 1, + } + accountCurrency = &types.AccountCurrency{ + Account: &types.AccountIdentifier{ + Address: "addr 1", + }, + Currency: &types.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + } + ) + + mockHelper := &mocks.Helper{} + mockHandler := &mocks.Handler{} + opts := []Option{ + WithActiveConcurrency(0), + WithInactiveConcurrency(1), + WithSeenAccounts([]*types.AccountCurrency{accountCurrency}), + WithDebugLogging(), + WithInactiveFrequency(10), + WithLookupBalanceByBlock(), + } + r := New( + mockHelper, + mockHandler, + nil, + opts..., + ) + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // Reconcile initially + mtxn := &mockDatabase.Transaction{} + mtxn.On("Discard", mock.Anything).Once() + mockHelper.On("DatabaseTransaction", mock.Anything).Return(mtxn).Once() + mockHelper.On("CurrentBlock", mock.Anything, mtxn).Return(block, nil).Once() + mtxn2 := &mockDatabase.Transaction{} + mtxn2.On( + "Discard", + mock.Anything, + ).Once() + mockHelper.On("DatabaseTransaction", mock.Anything).Return(mtxn2).Once() + mockReconcilerCalls( + mockHelper, + mockHandler, + mtxn2, + true, + accountCurrency, + "100", + "100", + block, + block, + true, + InactiveReconciliation, + nil, + false, + false, + ) + + // Force Rreconciliation eventhough not required + mtxn3 := &mockDatabase.Transaction{} + mtxn3.On("Discard", mock.Anything).Once() + mockHelper.On("DatabaseTransaction", mock.Anything).Return(mtxn3).Once() + mockHelper.On("CurrentBlock", mock.Anything, mtxn3).Return(block, nil).Once() + + mtxn4 := &mockDatabase.Transaction{} + mtxn4.On("Discard", mock.Anything).Run( + func(args mock.Arguments) { + cancel() + }, + ).Once() + mockHelper.On("DatabaseTransaction", mock.Anything).Return(mtxn4).Once() + mockHelper.On( + "ForceInactiveReconciliation", + mock.Anything, + accountCurrency.Account, + accountCurrency.Currency, + block, + ).Return( + true, + ).Once() + mockReconcilerCalls( + mockHelper, + mockHandler, + mtxn4, + true, + accountCurrency, + "100", + "100", + block, + block, + true, + InactiveReconciliation, + nil, + false, + false, + ) + err := r.Reconcile(ctx) + assert.Contains(t, context.Canceled.Error(), err.Error()) +} diff --git a/reconciler/types.go b/reconciler/types.go index 0e9c4abe2..808a4644f 100644 --- a/reconciler/types.go +++ b/reconciler/types.go @@ -168,6 +168,19 @@ type Helper interface { currency *types.Currency, index int64, ) error + + // ForceInactiveReconciliation is invoked by the + // inactive reconciler when the next account to + // reconcile has been checked within the configured + // inactive reconciliation frequency. This allows + // the helper to dynamically force inactive reconciliation + // when desired (i.e. when at tip). + ForceInactiveReconciliation( + ctx context.Context, + account *types.AccountIdentifier, + currency *types.Currency, + lastCheck *types.BlockIdentifier, + ) bool } // Handler is called by Reconciler after a reconciliation