Skip to content

Commit 76c63db

Browse files
authored
Merge pull request #939 from ellemouton/sql3Accounts3
[sql-3] accounts: replace calls to UpdateAccount
2 parents e2c4da2 + a4506c9 commit 76c63db

File tree

6 files changed

+509
-110
lines changed

6 files changed

+509
-110
lines changed

accounts/errors.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,17 @@ var (
88
ErrLabelAlreadyExists = errors.New(
99
"account label uniqueness constraint violation",
1010
)
11+
12+
// ErrAlreadySucceeded is returned by the UpsertAccountPayment method
13+
// if the WithErrAlreadySucceeded option is used and the payment has
14+
// already succeeded.
15+
ErrAlreadySucceeded = errors.New("payment has already succeeded")
16+
17+
// ErrPaymentNotAssociated indicate that the payment with the given hash
18+
// has not yet been associated with the account in question. It is also
19+
// returned when the WithErrIfUnknown option is used with
20+
// UpsertAccountPayment if the payment is not yet known.
21+
ErrPaymentNotAssociated = errors.New(
22+
"payment not associated with account",
23+
)
1124
)

accounts/interface.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,23 @@ type Store interface {
235235
IncreaseAccountBalance(ctx context.Context, id AccountID,
236236
amount lnwire.MilliSatoshi) error
237237

238+
// UpsertAccountPayment updates or inserts a payment entry for the given
239+
// account. Various functional options can be passed to modify the
240+
// behavior of the method. The returned boolean is true if the payment
241+
// was already known before the update. This is to be treated as a
242+
// best-effort indication if an error is also returned since the method
243+
// may error before the boolean can be set correctly.
244+
UpsertAccountPayment(_ context.Context, id AccountID,
245+
paymentHash lntypes.Hash, fullAmount lnwire.MilliSatoshi,
246+
status lnrpc.Payment_PaymentStatus,
247+
options ...UpsertPaymentOption) (bool, error)
248+
249+
// DeleteAccountPayment removes a payment entry from the account with
250+
// the given ID. It will return the ErrPaymentNotAssociated error if the
251+
// payment is not associated with the account.
252+
DeleteAccountPayment(_ context.Context, id AccountID,
253+
hash lntypes.Hash) error
254+
238255
// RemoveAccount finds an account by its ID and removes it from the¨
239256
// store.
240257
RemoveAccount(ctx context.Context, id AccountID) error
@@ -316,3 +333,75 @@ type RequestValuesStore interface {
316333
// DeleteValues deletes any values stored for the given request ID.
317334
DeleteValues(reqID uint64)
318335
}
336+
337+
// UpsertPaymentOption is a functional option that can be passed to the
338+
// UpsertAccountPayment method to modify its behavior.
339+
type UpsertPaymentOption func(*upsertAcctPaymentOption)
340+
341+
// upsertAcctPaymentOption is a struct that holds optional parameters for the
342+
// UpsertAccountPayment method.
343+
type upsertAcctPaymentOption struct {
344+
debitAccount bool
345+
errIfAlreadyPending bool
346+
usePendingAmount bool
347+
errIfAlreadySucceeded bool
348+
errIfUnknown bool
349+
}
350+
351+
// newUpsertPaymentOption creates a new upsertAcctPaymentOption with default
352+
// values.
353+
func newUpsertPaymentOption() *upsertAcctPaymentOption {
354+
return &upsertAcctPaymentOption{
355+
debitAccount: false,
356+
errIfAlreadyPending: false,
357+
usePendingAmount: false,
358+
errIfAlreadySucceeded: false,
359+
errIfUnknown: false,
360+
}
361+
}
362+
363+
// WithDebitAccount is a functional option that can be passed to the
364+
// UpsertAccountPayment method to indicate that the account balance should be
365+
// debited by the full amount of the payment.
366+
func WithDebitAccount() UpsertPaymentOption {
367+
return func(o *upsertAcctPaymentOption) {
368+
o.debitAccount = true
369+
}
370+
}
371+
372+
// WithErrIfAlreadyPending is a functional option that can be passed to the
373+
// UpsertAccountPayment method to indicate that an error should be returned if
374+
// the payment is already pending or succeeded.
375+
func WithErrIfAlreadyPending() UpsertPaymentOption {
376+
return func(o *upsertAcctPaymentOption) {
377+
o.errIfAlreadyPending = true
378+
}
379+
}
380+
381+
// WithErrIfAlreadySucceeded is a functional option that can be passed to the
382+
// UpsertAccountPayment method to indicate that the ErrAlreadySucceeded error
383+
// should be returned if the payment is already in a succeeded state.
384+
func WithErrIfAlreadySucceeded() UpsertPaymentOption {
385+
return func(o *upsertAcctPaymentOption) {
386+
o.errIfAlreadySucceeded = true
387+
}
388+
}
389+
390+
// WithPendingAmount is a functional option that can be passed to the
391+
// UpsertAccountPayment method to indicate that if the payment already exists,
392+
// then the known payment amount should be used instead of the new value passed
393+
// to the method.
394+
func WithPendingAmount() UpsertPaymentOption {
395+
return func(o *upsertAcctPaymentOption) {
396+
o.usePendingAmount = true
397+
}
398+
}
399+
400+
// WithErrIfUnknown is a functional option that can be passed to the
401+
// UpsertAccountPayment method to indicate that the ErrPaymentNotAssociated
402+
// error should be returned if the payment is not associated with the account.
403+
func WithErrIfUnknown() UpsertPaymentOption {
404+
return func(o *upsertAcctPaymentOption) {
405+
o.errIfUnknown = true
406+
}
407+
}

accounts/service.go

Lines changed: 63 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -467,26 +467,7 @@ func (s *InterceptorService) PaymentErrored(ctx context.Context, id AccountID,
467467
"has already started")
468468
}
469469

470-
account, err := s.store.Account(ctx, id)
471-
if err != nil {
472-
return err
473-
}
474-
475-
// Check that this payment is actually associated with this account.
476-
_, ok = account.Payments[hash]
477-
if !ok {
478-
return fmt.Errorf("payment with hash %s is not associated "+
479-
"with this account", hash)
480-
}
481-
482-
// Delete the payment and update the persisted account.
483-
delete(account.Payments, hash)
484-
485-
if err := s.store.UpdateAccount(ctx, account); err != nil {
486-
return fmt.Errorf("error updating account: %w", err)
487-
}
488-
489-
return nil
470+
return s.store.DeleteAccountPayment(ctx, id, hash)
490471
}
491472

492473
// AssociatePayment associates a payment (hash) with the given account,
@@ -498,44 +479,24 @@ func (s *InterceptorService) AssociatePayment(ctx context.Context, id AccountID,
498479
s.Lock()
499480
defer s.Unlock()
500481

501-
account, err := s.store.Account(ctx, id)
502-
if err != nil {
503-
return err
504-
}
505-
506-
// Check if this payment is associated with the account already.
507-
_, ok := account.Payments[paymentHash]
508-
if ok {
509-
// We do not allow another payment to the same hash if the
510-
// payment is already in-flight or succeeded. This mitigates a
511-
// user being able to launch a second RPC-erring payment with
512-
// the same hash that would remove the payment from being
513-
// tracked. Note that this prevents launching multipart
514-
// payments, but allows retrying a payment if it has failed.
515-
if account.Payments[paymentHash].Status !=
516-
lnrpc.Payment_FAILED {
517-
518-
return fmt.Errorf("payment with hash %s is already in "+
519-
"flight or succeeded (status %v)", paymentHash,
520-
account.Payments[paymentHash].Status)
521-
}
522-
523-
// Otherwise, we fall through to correctly update the payment
524-
// amount, in case we have a zero-amount invoice that is
525-
// retried.
526-
}
527-
528-
// Associate the payment with the account and store it.
529-
account.Payments[paymentHash] = &PaymentEntry{
530-
Status: lnrpc.Payment_UNKNOWN,
531-
FullAmount: fullAmt,
532-
}
533-
534-
if err := s.store.UpdateAccount(ctx, account); err != nil {
535-
return fmt.Errorf("error updating account: %w", err)
536-
}
482+
// We add the WithErrIfAlreadyPending option to ensure that if the
483+
// payment is already associated with the account, then we return
484+
// an error if the payment is already in-flight or succeeded. This
485+
// prevents a user from being able to launch a second RPC-erring payment
486+
// with the same hash that would remove the payment from being tracked.
487+
//
488+
// NOTE: this prevents launching multipart payments, but allows
489+
// retrying a payment if it has failed.
490+
//
491+
// If the payment is already associated with the account but not in
492+
// flight, we update the payment amount in case we have a zero-amount
493+
// invoice that is retried.
494+
_, err := s.store.UpsertAccountPayment(
495+
ctx, id, paymentHash, fullAmt, lnrpc.Payment_UNKNOWN,
496+
WithErrIfAlreadyPending(),
497+
)
537498

538-
return nil
499+
return err
539500
}
540501

541502
// invoiceUpdate credits the account an invoice was registered with, in case the
@@ -629,34 +590,30 @@ func (s *InterceptorService) TrackPayment(ctx context.Context, id AccountID,
629590
return nil
630591
}
631592

632-
// Similarly, if we've already processed the payment in the past, there
633-
// is a reference in the account with the given state.
634-
account, err := s.store.Account(ctx, id)
635-
if err != nil {
636-
return fmt.Errorf("error fetching account: %w", err)
637-
}
638-
639593
// If the account already stored a terminal state, we also don't need to
640-
// track the payment again.
641-
entry, ok := account.Payments[hash]
642-
if ok && successState(entry.Status) {
643-
return nil
594+
// track the payment again. So we add the WithErrIfAlreadySucceeded
595+
// option to ensure that we return an error if the payment has already
596+
// succeeded. We can then match on the ErrAlreadySucceeded error and
597+
// exit early if it is returned.
598+
opts := []UpsertPaymentOption{
599+
WithErrIfAlreadySucceeded(),
644600
}
645601

646602
// There is a case where the passed in fullAmt is zero but the pending
647603
// amount is not. In that case, we should not overwrite the pending
648604
// amount.
649605
if fullAmt == 0 {
650-
fullAmt = entry.FullAmount
651-
}
652-
653-
account.Payments[hash] = &PaymentEntry{
654-
Status: lnrpc.Payment_UNKNOWN,
655-
FullAmount: fullAmt,
606+
opts = append(opts, WithPendingAmount())
656607
}
657608

658-
if err := s.store.UpdateAccount(ctx, account); err != nil {
659-
if !ok {
609+
known, err := s.store.UpsertAccountPayment(
610+
ctx, id, hash, fullAmt, lnrpc.Payment_UNKNOWN, opts...,
611+
)
612+
if err != nil {
613+
if errors.Is(err, ErrAlreadySucceeded) {
614+
return nil
615+
}
616+
if !known {
660617
// In the rare case that the payment isn't associated
661618
// with an account yet, and we fail to update the
662619
// account we will not be tracking the payment, even if
@@ -832,23 +789,14 @@ func (s *InterceptorService) paymentUpdate(ctx context.Context,
832789

833790
// The payment went through! We now need to debit the full amount from
834791
// the account.
835-
account, err := s.store.Account(ctx, pendingPayment.accountID)
836-
if err != nil {
837-
err = s.disableAndErrorfUnsafe("error fetching account: %w",
838-
err)
839-
840-
return terminalState, err
841-
}
842-
843792
fullAmount := status.Value + status.Fee
844793

845-
// Update the account and store it in the database.
846-
account.CurrentBalance -= int64(fullAmount)
847-
account.Payments[hash] = &PaymentEntry{
848-
Status: lnrpc.Payment_SUCCEEDED,
849-
FullAmount: fullAmount,
850-
}
851-
if err := s.store.UpdateAccount(ctx, account); err != nil {
794+
// Update the persisted account.
795+
_, err := s.store.UpsertAccountPayment(
796+
ctx, pendingPayment.accountID, hash, fullAmount,
797+
lnrpc.Payment_SUCCEEDED, WithDebitAccount(),
798+
)
799+
if err != nil {
852800
err = s.disableAndErrorfUnsafe("error updating account: %w",
853801
err)
854802

@@ -892,23 +840,36 @@ func (s *InterceptorService) removePayment(ctx context.Context,
892840
return nil
893841
}
894842

895-
account, err := s.store.Account(ctx, pendingPayment.accountID)
896-
if err != nil {
897-
return err
843+
_, err := s.store.UpsertAccountPayment(
844+
ctx, pendingPayment.accountID, hash, 0, status,
845+
// We don't want the payment to be inserted if it isn't already
846+
// known. So we pass in this option to ensure that the call
847+
// exits early if the payment is unknown.
848+
WithErrIfUnknown(),
849+
// Otherwise, we just want to update the status of the payment
850+
// and use the existing pending amount.
851+
WithPendingAmount(),
852+
)
853+
if err != nil && !errors.Is(err, ErrPaymentNotAssociated) {
854+
return fmt.Errorf("error updating account: %w", err)
898855
}
899856

900857
pendingPayment.cancel()
901858
delete(s.pendingPayments, hash)
902859

903-
// Have we associated the payment with the account already?
904-
_, ok = account.Payments[hash]
905-
if !ok {
906-
return nil
907-
}
860+
return nil
861+
}
908862

909-
// If we did, let's set the status correctly in the DB now.
910-
account.Payments[hash].Status = status
911-
return s.store.UpdateAccount(ctx, account)
863+
// hasPayment returns true if the payment is currently being tracked by the
864+
// service.
865+
//
866+
// NOTE: this is currently used only for tests.
867+
func (s *InterceptorService) hasPayment(hash lntypes.Hash) bool {
868+
s.RLock()
869+
defer s.RUnlock()
870+
871+
_, ok := s.pendingPayments[hash]
872+
return ok
912873
}
913874

914875
// successState returns true if a payment was completed successfully.

accounts/service_test.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,14 @@ func TestAccountService(t *testing.T) {
383383
// Assert that the invoice subscription succeeded.
384384
require.Contains(t, s.invoiceToAccount, testHash)
385385

386-
// But setting up the payment tracking should have failed.
386+
// But setting up the payment tracking should have
387+
// failed.
387388
require.False(t, s.IsRunning())
388389

389-
// Finally let's assert that we didn't successfully add the
390-
// payment to pending payment, and that lnd isn't awaiting
391-
// the payment request.
392-
require.NotContains(t, s.pendingPayments, testHash)
390+
// Finally let's assert that we didn't successfully add
391+
// the payment to pending payment, and that lnd isn't
392+
// awaiting the payment request.
393+
require.False(t, s.hasPayment(testHash))
393394
r.assertNoPaymentRequest(t)
394395
},
395396
}, {
@@ -426,7 +427,9 @@ func TestAccountService(t *testing.T) {
426427
// This will cause an error send an update over
427428
// the payment channel, which should disable the
428429
// service.
429-
s.pendingPayments = make(map[lntypes.Hash]*trackedPayment)
430+
s.pendingPayments = make(
431+
map[lntypes.Hash]*trackedPayment,
432+
)
430433

431434
// Send an invalid payment over the payment chan
432435
// which should error and disable the service
@@ -568,7 +571,7 @@ func TestAccountService(t *testing.T) {
568571
return p.Status == lnrpc.Payment_FAILED
569572
})
570573

571-
require.NotContains(t, s.pendingPayments, testHash2)
574+
require.False(t, s.hasPayment(testHash2))
572575

573576
// Finally, if an unknown payment turns out to be
574577
// a non-initiated payment, we should stop the tracking
@@ -616,7 +619,7 @@ func TestAccountService(t *testing.T) {
616619

617620
// Ensure that the payment was removed from the pending
618621
// payments.
619-
require.NotContains(t, s.pendingPayments, testHash3)
622+
require.False(t, s.hasPayment(testHash3))
620623
},
621624
}, {
622625
name: "keep track of invoice indexes",

0 commit comments

Comments
 (0)