Skip to content
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

feat(backend): improve PSQL balance calculation #2881

Merged
merged 4 commits into from
Aug 26, 2024

Conversation

mkurapov
Copy link
Contributor

@mkurapov mkurapov commented Aug 21, 2024

Changes proposed in this pull request

  • For PSQL accounting, instead of calculating balances by fetching all transfers for an account and then doing the balance calculation in memory, do the calculation in PSQL
  • Add constraint to ledgerAccounts table to prevent any PENDING transfers being created without an expiryDate (to avoid having "hanging" pending transfers that will never expire)

Running the perf test script with PSQL DB (graph showing transaction counts):

Before:

Screenshot 2024-08-20 at 21 13 00

After:

Screenshot 2024-08-20 at 21 43 19

Context

Fixes #2879

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Documentation added
  • Make sure that all checks pass
  • Bruno collection updated

@mkurapov mkurapov changed the title 2879/mk/updated balances feat(backend): improve balance calculation Aug 21, 2024
@github-actions github-actions bot added type: tests Testing related pkg: backend Changes in the backend package. type: source Changes business logic labels Aug 21, 2024
Copy link

netlify bot commented Aug 21, 2024

Deploy Preview for brilliant-pasca-3e80ec canceled.

Name Link
🔨 Latest commit fe5c167
🔍 Latest deploy log https://app.netlify.com/sites/brilliant-pasca-3e80ec/deploys/66c8b6c64360dd00088cc639

@mkurapov mkurapov linked an issue Aug 21, 2024 that may be closed by this pull request
@mkurapov mkurapov marked this pull request as ready for review August 23, 2024 15:17
Comment on lines +48 to +54
deps.logger.error(
{
err,
accountId: account.id
},
'Could not fetch balances for account'
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not doing much with the error here (not expecting it to happen), just logging the accountId we were trying to query

Comment on lines +43 to +45
(state === LedgerTransferState.PENDING
? new Date(Date.now() + 86_400_000)
: undefined),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only used for tests, we just want to avoid breaking the newly added constraint above

@mkurapov mkurapov requested review from koekiebox, BlairCurrey, golobitch and njlie and removed request for koekiebox August 23, 2024 15:21
@mkurapov mkurapov changed the title feat(backend): improve balance calculation feat(backend): improve PSQL balance calculation Aug 23, 2024
@@ -16,38 +14,45 @@ export async function getAccountBalances(
account: LedgerAccount,
trx?: TransactionOrKnex
): Promise<AccountBalance> {
const { credits, debits } = await getAccountTransfers(
Copy link
Contributor

@BlairCurrey BlairCurrey Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can tackle this in another issue but can we get rid of getAccountTransfers now? I think it's only exposed in a recently added gql resolver. That method still brings the records into code and iterate over all of them (so like half of the total problem you fixed here). So I would still expect performance issues as transfer count grows where its used (?).

IDK if we want to:

A) leave it as-is because we dont think the performance issue will matter much. It's possible. I dont think the use-case for the resolver is well defined. I guess its just so people can see each transfer from the admin ui (not currently implemented and not sure what they would do with that info).
B) Remove it
C) Also rework that query. Not exactly sure how much we would leverage this one since its aggregating it a bit differently.

I guess I would say B or C but im not sure its worth the trouble of refactoring if we dont have a clear use case for consuming it, so leaning B.

Copy link
Contributor Author

@mkurapov mkurapov Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can tackle this in another issue but can we get rid of getAccountTransfers now? I think it's only exposed in a recently added gql resolver

Yes, I think the gql is worth keeping and since its used more from dev/admin point of view and not something getting called every trx, its like you said, not a big deal, and we can leave as is for now.

Adding pagination (so we grab only a limited set of transactions) would be a good first step in improving it IMO (also just having a default low limit is not bad as well)

exports.up = function (knex) {
return knex.schema.table('ledgerTransfers', function (table) {
table.check(
`("state" != 'PENDING') OR ("expiresAt" IS NOT NULL)`,
Copy link
Contributor

@BlairCurrey BlairCurrey Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the new query error if some record doesnt satisfy the constraint (hence why you added it)? Just wondering if we can guaruntee all existing records will pass this constraint... migration will fail otherwise. Im also wondering if the error is intelligible if we try to add one that doesnt pass this constraint. Not really sure where that would happen or how it should surface...

Copy link
Contributor Author

@mkurapov mkurapov Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking rafiki money would be a good test, but I think it's not actually something that should have ever happened, this is how we map all transfers before creating them:

function prepareTransfer(
transfer: CreateLedgerTransferArgs
): Partial<LedgerTransfer> {
return {
amount: transfer.amount,
transferRef: transfer.transferRef,
creditAccountId: transfer.creditAccount.id,
debitAccountId: transfer.debitAccount.id,
ledger: transfer.creditAccount.ledger,
state: transfer.timeoutMs
? LedgerTransferState.PENDING
: LedgerTransferState.POSTED,
expiresAt: transfer.timeoutMs
? new Date(Date.now() + Number(transfer.timeoutMs))
: undefined,
type: transfer.type
}

i.e. if the timeout is provided, we set PENDING with expiryAt always

In terms of error legibility, I think it should be properly serviced up when we create the transfers in the psql transaction.

I also think this is mostly something to protect against refactoring code - this would be more a dev error than anything

Copy link
Contributor Author

@mkurapov mkurapov Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rafiki.money db:

rafiki_backend=# select count(*) from "ledgerTransfers" where state = 'PENDING' and "expiresAt" is null;
 count 
-------
     0
(1 row)

} else if (credit.state === LedgerTransferState.PENDING) {
creditsPending += credit.amount
if (queryResult?.rows < 1) {
throw new Error('No results when fetching balance for account')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was always wondered how do we decide if we should throw error, or just return empty response (in case of a balance, 0)?

Copy link
Contributor

@BlairCurrey BlairCurrey Aug 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your question still stands but I played around with it because I wasnt sure how it would work and wanted to add some context. When there are no records with the associated id it still aggregates a result, its just 0 for all the balances. So I think like Max mentioned in his other comment, this could probably never happen. But yeah I think it still begs the question, do we need to error or can we just return 0 balances (logging for sure either way).

I guess since it's totally unexpected I'm not sure indicating the balances are 0 would be correct. Not entirely sure how this is consumed but it might send the wrong signal.

Copy link
Contributor Author

@mkurapov mkurapov Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is if results of the query weren't able to be returned at all - so missing information unexpectedly, a result of a psql issue. So in that case we should throw and stop everything IMO

@mkurapov mkurapov merged commit 0248bc2 into main Aug 26, 2024
42 checks passed
@mkurapov mkurapov deleted the 2879/mk/updated-balances branch August 26, 2024 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: backend Changes in the backend package. type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve PSQL account balance calculation
3 participants