Merge pull request #8081 from bhandras/invoicedb-refactor

invoices: refactor `InvoiceDB` to eliminate `ScanInvoices`
This commit is contained in:
Oliver Gugger authored and GitHub committed 2023-10-19 09:27:54 +00:00
commit 812f4d9242
6 files changed
+253 -126

No files matched your search

+95 -42
View File
@@ -1013,64 +1013,58 @@ func TestSettleIndexAmpPayments(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
} }
// TestScanInvoices tests that ScanInvoices scans through all stored invoices // TestFetchPendingInvoices tests that we can fetch all pending invoices from
// correctly. // the database using the FetchPendingInvoices method.
func TestScanInvoices(t *testing.T) { func TestFetchPendingInvoices(t *testing.T) {
t.Parallel() t.Parallel()
db, err := MakeTestInvoiceDB(t) db, err := MakeTestInvoiceDB(t, OptionClock(testClock))
require.NoError(t, err, "unable to make test db") require.NoError(t, err, "unable to make test db")
var invoices map[lntypes.Hash]*invpkg.Invoice
callCount := 0
resetCount := 0
// reset is used to reset/initialize results and is called once
// upon calling ScanInvoices and when the underlying transaction is
// retried.
reset := func() {
invoices = make(map[lntypes.Hash]*invpkg.Invoice)
callCount = 0
resetCount++
}
scanFunc := func(paymentHash lntypes.Hash,
invoice *invpkg.Invoice) error {
invoices[paymentHash] = invoice
callCount++
return nil
}
ctxb := context.Background() ctxb := context.Background()
// With an empty DB we expect to not scan any invoices.
require.NoError(t, db.ScanInvoices(ctxb, scanFunc, reset))
require.Equal(t, 0, len(invoices))
require.Equal(t, 0, callCount)
require.Equal(t, 1, resetCount)
numInvoices := 5 // Make sure that fetching pending invoices from an empty database
testInvoices := make(map[lntypes.Hash]*invpkg.Invoice) // returns an empty result and no errors.
pending, err := db.FetchPendingInvoices(ctxb)
require.NoError(t, err)
require.Empty(t, pending)
const numInvoices = 20
var settleIndex uint64 = 1
pendingInvoices := make(map[lntypes.Hash]invpkg.Invoice)
// Now populate the DB and check if we can get all invoices with their
// payment hashes as expected.
for i := 1; i <= numInvoices; i++ { for i := 1; i <= numInvoices; i++ {
invoice, err := randInvoice(lnwire.MilliSatoshi(i)) amt := lnwire.MilliSatoshi(i * 1000)
invoice, err := randInvoice(amt)
require.NoError(t, err) require.NoError(t, err)
invoice.CreationDate = invoice.CreationDate.Add(
time.Duration(i-1) * time.Second,
)
paymentHash := invoice.Terms.PaymentPreimage.Hash() paymentHash := invoice.Terms.PaymentPreimage.Hash()
testInvoices[paymentHash] = invoice
_, err = db.AddInvoice(ctxb, invoice, paymentHash) _, err = db.AddInvoice(ctxb, invoice, paymentHash)
require.NoError(t, err) require.NoError(t, err)
// Settle every second invoice.
if i%2 == 0 {
pendingInvoices[paymentHash] = *invoice
continue
}
ref := invpkg.InvoiceRefByHash(paymentHash)
_, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(amt))
require.NoError(t, err)
settleTestInvoice(invoice, settleIndex)
settleIndex++
} }
resetCount = 0 // Fetch all pending invoices.
require.NoError(t, db.ScanInvoices(ctxb, scanFunc, reset)) pending, err = db.FetchPendingInvoices(ctxb)
require.Equal(t, numInvoices, callCount) require.NoError(t, err)
require.Equal(t, testInvoices, invoices) require.Equal(t, pendingInvoices, pending)
require.Equal(t, 1, resetCount)
} }
// TestDuplicateSettleInvoice tests that if we add a new invoice and settle it // TestDuplicateSettleInvoice tests that if we add a new invoice and settle it
@@ -3092,6 +3086,65 @@ func TestDeleteInvoices(t *testing.T) {
assertInvoiceCount(0) assertInvoiceCount(0)
} }
// TestDeleteCanceledInvoices tests that deleting canceled invoices with the
// specific DeleteCanceledInvoices method works correctly.
func TestDeleteCanceledInvoices(t *testing.T) {
t.Parallel()
db, err := MakeTestInvoiceDB(t)
require.NoError(t, err, "unable to make test db")
// Updatefunc is used to cancel an invoice.
updateFunc := func(invoice *invpkg.Invoice) (
*invpkg.InvoiceUpdateDesc, error) {
return &invpkg.InvoiceUpdateDesc{
UpdateType: invpkg.CancelInvoiceUpdate,
State: &invpkg.InvoiceStateUpdateDesc{
NewState: invpkg.ContractCanceled,
},
}, nil
}
// Add some invoices to the test db.
ctxb := context.Background()
var invoices []invpkg.Invoice
for i := 0; i < 10; i++ {
invoice, err := randInvoice(lnwire.MilliSatoshi(i + 1))
require.NoError(t, err)
paymentHash := invoice.Terms.PaymentPreimage.Hash()
_, err = db.AddInvoice(ctxb, invoice, paymentHash)
require.NoError(t, err)
// Cancel every second invoice.
if i%2 == 0 {
invoice, err = db.UpdateInvoice(
ctxb, invpkg.InvoiceRefByHash(paymentHash), nil,
updateFunc,
)
require.NoError(t, err)
} else {
invoices = append(invoices, *invoice)
}
}
// Delete canceled invoices.
require.NoError(t, db.DeleteCanceledInvoices(ctxb))
// Query to collect all invoices.
query := invpkg.InvoiceQuery{
IndexOffset: 0,
NumMaxInvoices: math.MaxUint64,
}
dbInvoices, err := db.QueryInvoices(ctxb, query)
require.NoError(t, err)
// Check that we really have the expected invoices.
require.Equal(t, invoices, dbInvoices.Invoices)
}
// TestAddInvoiceInvalidFeatureDeps asserts that inserting an invoice with // TestAddInvoiceInvalidFeatureDeps asserts that inserting an invoice with
// invalid transitive feature dependencies fails with the appropriate error. // invalid transitive feature dependencies fails with the appropriate error.
func TestAddInvoiceInvalidFeatureDeps(t *testing.T) { func TestAddInvoiceInvalidFeatureDeps(t *testing.T) {
+120 -12
View File
@@ -433,17 +433,18 @@ func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex, setIDIndex kvdb.RBucket,
} }
} }
// ScanInvoices scans through all invoices and calls the passed scanFunc for // FetchPendingInvoices returns all invoices that have not yet been settled or
// for each invoice with its respective payment hash. Additionally a reset() // canceled. The returned map is keyed by the payment hash of each respective
// closure is passed which is used to reset/initialize partial results and also // invoice.
// to signal if the kvdb.View transaction has been retried. func (d *DB) FetchPendingInvoices(_ context.Context) (
func (d *DB) ScanInvoices(_ context.Context, scanFunc invpkg.InvScanFunc, map[lntypes.Hash]invpkg.Invoice, error) {
reset func()) error {
return kvdb.View(d, func(tx kvdb.RTx) error { result := make(map[lntypes.Hash]invpkg.Invoice)
err := kvdb.View(d, func(tx kvdb.RTx) error {
invoices := tx.ReadBucket(invoiceBucket) invoices := tx.ReadBucket(invoiceBucket)
if invoices == nil { if invoices == nil {
return invpkg.ErrNoInvoicesCreated return nil
} }
invoiceIndex := invoices.NestedReadBucket(invoiceIndexBucket) invoiceIndex := invoices.NestedReadBucket(invoiceIndexBucket)
@@ -472,12 +473,23 @@ func (d *DB) ScanInvoices(_ context.Context, scanFunc invpkg.InvScanFunc,
return err return err
} }
var paymentHash lntypes.Hash if invoice.IsPending() {
copy(paymentHash[:], k) var paymentHash lntypes.Hash
copy(paymentHash[:], k)
result[paymentHash] = invoice
}
return scanFunc(paymentHash, &invoice) return nil
}) })
}, reset) }, func() {
result = make(map[lntypes.Hash]invpkg.Invoice)
})
if err != nil {
return nil, err
}
return result, nil
} }
// QueryInvoices allows a caller to query the invoice database for invoices // QueryInvoices allows a caller to query the invoice database for invoices
@@ -2702,6 +2714,102 @@ func delAMPSettleIndex(invoiceNum []byte, invoices,
return nil return nil
} }
// DeleteCanceledInvoices deletes all canceled invoices from the database.
func (d *DB) DeleteCanceledInvoices(_ context.Context) error {
return kvdb.Update(d, func(tx kvdb.RwTx) error {
invoices := tx.ReadWriteBucket(invoiceBucket)
if invoices == nil {
return nil
}
invoiceIndex := invoices.NestedReadWriteBucket(
invoiceIndexBucket,
)
if invoiceIndex == nil {
return invpkg.ErrNoInvoicesCreated
}
invoiceAddIndex := invoices.NestedReadWriteBucket(
addIndexBucket,
)
if invoiceAddIndex == nil {
return invpkg.ErrNoInvoicesCreated
}
payAddrIndex := tx.ReadWriteBucket(payAddrIndexBucket)
return invoiceIndex.ForEach(func(k, v []byte) error {
// Skip the special numInvoicesKey as that does not
// point to a valid invoice.
if bytes.Equal(k, numInvoicesKey) {
return nil
}
// Skip sub-buckets.
if v == nil {
return nil
}
invoice, err := fetchInvoice(v, invoices)
if err != nil {
return err
}
if invoice.State != invpkg.ContractCanceled {
return nil
}
// Delete the payment hash from the invoice index.
err = invoiceIndex.Delete(k)
if err != nil {
return err
}
// Delete payment address index reference if there's a
// valid payment address.
if invoice.Terms.PaymentAddr != invpkg.BlankPayAddr {
// To ensure consistency check that the already
// fetched invoice key matches the one in the
// payment address index.
key := payAddrIndex.Get(
invoice.Terms.PaymentAddr[:],
)
if bytes.Equal(key, k) {
// Delete from the payment address
// index.
if err := payAddrIndex.Delete(
invoice.Terms.PaymentAddr[:],
); err != nil {
return err
}
}
}
// Remove from the add index.
var addIndexKey [8]byte
byteOrder.PutUint64(addIndexKey[:], invoice.AddIndex)
err = invoiceAddIndex.Delete(addIndexKey[:])
if err != nil {
return err
}
// Note that we don't need to delete the invoice from
// the settle index as it is not added until the
// invoice is settled.
// Now remove all sub invoices.
err = delAMPInvoices(k, invoices)
if err != nil {
return err
}
// Finally remove the serialized invoice from the
// invoice bucket.
return invoices.Delete(k)
})
}, func() {})
}
// DeleteInvoice attempts to delete the passed invoices from the database in // DeleteInvoice attempts to delete the passed invoices from the database in
// one transaction. The passed delete references hold all keys required to // one transaction. The passed delete references hold all keys required to
// delete the invoices without also needing to deserialze them. // delete the invoices without also needing to deserialze them.
@@ -82,6 +82,9 @@
a context parameter to all `InvoiceDB` methods which is a pre-requisite for a context parameter to all `InvoiceDB` methods which is a pre-requisite for
the SQL implementation. the SQL implementation.
* [Refactor InvoiceDB](https://github.com/lightningnetwork/lnd/pull/8081) to
eliminate the use of `ScanInvoices`.
## Code Health ## Code Health
## Tooling and Documentation ## Tooling and Documentation
+8 -14
View File
@@ -9,10 +9,6 @@ import (
"github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/record"
) )
// InvScanFunc is a helper type used to specify the type used in the
// ScanInvoices methods (part of the InvoiceDB interface).
type InvScanFunc func(lntypes.Hash, *Invoice) error
// InvoiceDB is the database that stores the information about invoices. // InvoiceDB is the database that stores the information about invoices.
type InvoiceDB interface { type InvoiceDB interface {
// AddInvoice inserts the targeted invoice into the database. // AddInvoice inserts the targeted invoice into the database.
@@ -45,16 +41,10 @@ type InvoiceDB interface {
// payment. // payment.
LookupInvoice(ctx context.Context, ref InvoiceRef) (Invoice, error) LookupInvoice(ctx context.Context, ref InvoiceRef) (Invoice, error)
// ScanInvoices scans through all invoices and calls the passed scanFunc // FetchPendingInvoices returns all invoices that have not yet been
// for each invoice with its respective payment hash. Additionally a // settled or canceled.
// reset() closure is passed which is used to reset/initialize partial FetchPendingInvoices(ctx context.Context) (map[lntypes.Hash]Invoice,
// results and also to signal if the kvdb.View transaction has been error)
// retried.
//
// TODO(positiveblue): abstract this functionality so it makes sense for
// other backends like sql.
ScanInvoices(ctx context.Context, scanFunc InvScanFunc,
reset func()) error
// QueryInvoices allows a caller to query the invoice database for // QueryInvoices allows a caller to query the invoice database for
// invoices within the specified add index range. // invoices within the specified add index range.
@@ -90,6 +80,10 @@ type InvoiceDB interface {
// deserialze them. // deserialze them.
DeleteInvoice(ctx context.Context, DeleteInvoice(ctx context.Context,
invoicesToDelete []InvoiceDeleteRef) error invoicesToDelete []InvoiceDeleteRef) error
// DeleteCanceledInvoices removes all canceled invoices from the
// database.
DeleteCanceledInvoices(ctx context.Context) error
} }
// Payload abstracts access to any additional fields provided in the final hop's // Payload abstracts access to any additional fields provided in the final hop's
+15 -53
View File
@@ -181,68 +181,30 @@ func NewRegistry(idb InvoiceDB, expiryWatcher *InvoiceExpiryWatcher,
// to the invoice expiry watcher while also attempting to delete all canceled // to the invoice expiry watcher while also attempting to delete all canceled
// invoices. // invoices.
func (i *InvoiceRegistry) scanInvoicesOnStart(ctx context.Context) error { func (i *InvoiceRegistry) scanInvoicesOnStart(ctx context.Context) error {
var ( pendingInvoices, err := i.idb.FetchPendingInvoices(ctx)
pending []invoiceExpiry
removable []InvoiceDeleteRef
)
reset := func() {
// Zero out our results on start and if the scan is ever run
// more than once. This latter case can happen if the kvdb
// layer needs to retry the View transaction underneath (eg.
// using the etcd driver, where all transactions are allowed
// to retry for serializability).
pending = nil
removable = make([]InvoiceDeleteRef, 0)
}
scanFunc := func(paymentHash lntypes.Hash,
invoice *Invoice) error {
if invoice.IsPending() {
expiryRef := makeInvoiceExpiry(paymentHash, invoice)
if expiryRef != nil {
pending = append(pending, expiryRef)
}
} else if i.cfg.GcCanceledInvoicesOnStartup &&
invoice.State == ContractCanceled {
// Consider invoice for removal if it is already
// canceled. Invoices that are expired but not yet
// canceled, will be queued up for cancellation after
// startup and will be deleted afterwards.
ref := InvoiceDeleteRef{
PayHash: paymentHash,
AddIndex: invoice.AddIndex,
SettleIndex: invoice.SettleIndex,
}
if invoice.Terms.PaymentAddr != BlankPayAddr {
ref.PayAddr = &invoice.Terms.PaymentAddr
}
removable = append(removable, ref)
}
return nil
}
err := i.idb.ScanInvoices(ctx, scanFunc, reset)
if err != nil { if err != nil {
return err return err
} }
var pending []invoiceExpiry
for paymentHash, invoice := range pendingInvoices {
invoice := invoice
expiryRef := makeInvoiceExpiry(paymentHash, &invoice)
if expiryRef != nil {
pending = append(pending, expiryRef)
}
}
log.Debugf("Adding %d pending invoices to the expiry watcher", log.Debugf("Adding %d pending invoices to the expiry watcher",
len(pending)) len(pending))
i.expiryWatcher.AddInvoices(pending...) i.expiryWatcher.AddInvoices(pending...)
if len(removable) > 0 { if i.cfg.GcCanceledInvoicesOnStartup {
log.Infof("Attempting to delete %v canceled invoices", log.Infof("Deleting canceled invoices")
len(removable)) err = i.idb.DeleteCanceledInvoices(ctx)
if err := i.idb.DeleteInvoice(ctx, removable); err != nil { if err != nil {
log.Warnf("Deleting canceled invoices failed: %v", err) log.Warnf("Deleting canceled invoices failed: %v", err)
} else { return err
log.Infof("Deleted %v canceled invoices",
len(removable))
} }
} }
+12 -5
View File
@@ -1,6 +1,8 @@
package invoices package invoices
import ( import (
"context"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -47,12 +49,11 @@ func (m *MockInvoiceDB) LookupInvoice(ref InvoiceRef) (Invoice, error) {
return invoice, args.Error(1) return invoice, args.Error(1)
} }
func (m *MockInvoiceDB) ScanInvoices(scanFunc InvScanFunc, func (m *MockInvoiceDB) FetchPendingInvoices(ctx context.Context) (
reset func()) error { map[lntypes.Hash]Invoice, error) {
args := m.Called(scanFunc, reset) args := m.Called(ctx)
return args.Get(0).(map[lntypes.Hash]Invoice), args.Error(1)
return args.Error(0)
} }
func (m *MockInvoiceDB) QueryInvoices(q InvoiceQuery) (InvoiceSlice, error) { func (m *MockInvoiceDB) QueryInvoices(q InvoiceQuery) (InvoiceSlice, error) {
@@ -76,3 +77,9 @@ func (m *MockInvoiceDB) DeleteInvoice(invoices []InvoiceDeleteRef) error {
return args.Error(0) return args.Error(0)
} }
func (m *MockInvoiceDB) DeleteCanceledInvoices(ctx context.Context) error {
args := m.Called(ctx)
return args.Error(0)
}