paymentsdb: implement FetchInFlightPayments for sql backend

This commit is contained in:
ziggie committed 2026-02-25 08:02:16 +01:00
1 parent 82ee78c5ef
commit 969b00e4a5
5 files changed
+288 -40

No files matched your search

+218 -3
View File
@@ -53,7 +53,7 @@ type SQLQueries interface {
FetchHtlcAttemptsForPayments(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchHtlcAttemptsForPaymentsRow, error)
FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchHtlcAttemptResolutionsForPaymentsRow, error)
FetchAllInflightAttempts(ctx context.Context) ([]sqlc.PaymentHtlcAttempt, error)
FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error)
FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error)
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error)
@@ -165,8 +165,88 @@ func fetchPaymentWithCompleteData(ctx context.Context,
return buildPaymentFromBatchData(dbPayment, batchData)
}
// paymentsDetailsData holds all the batch-loaded data for multiple payments.
// This does not include the core payment and intent data which is fetched
// paymentsCompleteData holds the full payment data when batch loading base
// payment data and all the related data for a payment.
type paymentsCompleteData struct {
*paymentsBaseData
*paymentsDetailsData
}
// batchLoadPayments loads the full payment data for a batch of payment IDs.
func batchLoadPayments(ctx context.Context, cfg *sqldb.QueryConfig,
db SQLQueries, paymentIDs []int64) (*paymentsCompleteData, error) {
baseData, err := batchLoadpaymentsBaseData(ctx, cfg, db, paymentIDs)
if err != nil {
return nil, fmt.Errorf("failed to load payment base data: %w",
err)
}
batchData, err := batchLoadPaymentDetailsData(ctx, cfg, db, paymentIDs)
if err != nil {
return nil, fmt.Errorf("failed to load payment batch data: %w",
err)
}
return &paymentsCompleteData{
paymentsBaseData: baseData,
paymentsDetailsData: batchData,
}, nil
}
// paymentsBaseData holds the base payment and intent data for a batch of
// payments.
type paymentsBaseData struct {
// paymentsAndIntents maps payment ID to its payment and intent data.
paymentsAndIntents map[int64]sqlc.PaymentAndIntent
}
// batchLoadpaymentsBaseData loads the base payment and payment intent data for
// a batch of payment IDs. This complements loadPaymentsBatchData which loads
// related data (attempts, hops, custom records) but not the payment table
// and payment intent table data.
func batchLoadpaymentsBaseData(ctx context.Context,
cfg *sqldb.QueryConfig, db SQLQueries,
paymentIDs []int64) (*paymentsBaseData, error) {
baseData := &paymentsBaseData{
paymentsAndIntents: make(map[int64]sqlc.PaymentAndIntent),
}
if len(paymentIDs) == 0 {
return baseData, nil
}
err := sqldb.ExecuteBatchQuery(
ctx, cfg, paymentIDs,
func(id int64) int64 { return id },
func(ctx context.Context, ids []int64) (
[]sqlc.FetchPaymentsByIDsRow, error) {
records, err := db.FetchPaymentsByIDs(
ctx, ids,
)
return records, err
},
func(ctx context.Context,
payment sqlc.FetchPaymentsByIDsRow) error {
baseData.paymentsAndIntents[payment.ID] = payment
return nil
},
)
if err != nil {
return nil, fmt.Errorf("failed to fetch payment base "+
"data: %w", err)
}
return baseData, nil
}
// paymentsRelatedData holds all the batch-loaded data for multiple payments.
// This does not include the base payment and intent data which is fetched
// separately. It includes the additional data like attempts, hops, hop custom
// records, and route custom records.
type paymentsDetailsData struct {
@@ -874,6 +954,141 @@ func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) {
return mpPayment, nil
}
// FetchInFlightPayments retrieves all payments that have HTLC attempts
// currently in flight (not yet settled or failed). These are payments with at
// least one HTLC attempt that has been registered but has no resolution record.
//
// The SQLStore implementation provides a significant performance improvement
// over the KVStore implementation by using targeted SQL queries instead of
// scanning all payments.
//
// This method is part of the PaymentReader interface, which is embedded in the
// DB interface. It's typically called during node startup to resume monitoring
// of pending payments and ensure HTLCs are properly tracked.
//
// TODO(ziggie): Consider changing the interface to use a callback or iterator
// pattern instead of returning all payments at once. This would allow
// processing payments one at a time without holding them all in memory
// simultaneously:
// - Callback: func FetchInFlightPayments(ctx, func(*MPPayment) error) error
// - Iterator: func FetchInFlightPayments(ctx) (PaymentIterator, error)
//
// While inflight payments are typically a small subset, this would improve
// memory efficiency for nodes with unusually high numbers of concurrent
// payments and would better leverage the existing pagination infrastructure.
func (s *SQLStore) FetchInFlightPayments() ([]*MPPayment,
error) {
ctx := context.TODO()
var mpPayments []*MPPayment
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
// Track which payment IDs we've already processed across all
// pages to avoid loading the same payment multiple times when
// multiple inflight attempts belong to the same payment.
processedPayments := make(map[int64]*MPPayment)
extractCursor := func(row sqlc.PaymentHtlcAttempt) int64 {
return row.AttemptIndex
}
// collectFunc extracts the payment ID from each attempt row.
collectFunc := func(row sqlc.PaymentHtlcAttempt) (
int64, error) {
return row.PaymentID, nil
}
// batchDataFunc loads payment data for a batch of payment IDs,
// but only for IDs we haven't processed yet.
batchDataFunc := func(ctx context.Context,
paymentIDs []int64) (*paymentsCompleteData, error) {
// Filter out already-processed payment IDs.
uniqueIDs := make([]int64, 0, len(paymentIDs))
for _, id := range paymentIDs {
_, processed := processedPayments[id]
if !processed {
uniqueIDs = append(uniqueIDs, id)
}
}
// If uniqueIDs is empty, the batch load will return
// empty batch data.
return batchLoadPayments(
ctx, s.cfg.QueryCfg, db, uniqueIDs,
)
}
// processAttempt processes each attempt. We only build and
// store the payment once per unique payment ID.
processAttempt := func(ctx context.Context,
row sqlc.PaymentHtlcAttempt,
batchData *paymentsCompleteData) error {
// Skip if we've already processed this payment.
_, processed := processedPayments[row.PaymentID]
if processed {
return nil
}
dbPayment := batchData.paymentsAndIntents[row.PaymentID]
// Build the payment from batch data.
mpPayment, err := buildPaymentFromBatchData(
dbPayment, batchData.paymentsDetailsData,
)
if err != nil {
return fmt.Errorf("failed to build payment: %w",
err)
}
// Store in our processed map.
processedPayments[row.PaymentID] = mpPayment
return nil
}
queryFunc := func(ctx context.Context, lastAttemptIndex int64,
limit int32) ([]sqlc.PaymentHtlcAttempt,
error) {
return db.FetchAllInflightAttempts(ctx,
sqlc.FetchAllInflightAttemptsParams{
AttemptIndex: lastAttemptIndex,
Limit: limit,
},
)
}
err := sqldb.ExecuteCollectAndBatchWithSharedDataQuery(
ctx, s.cfg.QueryCfg, int64(-1), queryFunc,
extractCursor, collectFunc, batchDataFunc,
processAttempt,
)
if err != nil {
return err
}
// Convert map to slice.
mpPayments = make([]*MPPayment, 0, len(processedPayments))
for _, payment := range processedPayments {
mpPayments = append(mpPayments, payment)
}
return nil
}, func() {
mpPayments = nil
})
if err != nil {
return nil, fmt.Errorf("failed to fetch inflight "+
"payments: %w", err)
}
return mpPayments, nil
}
// DeleteFailedAttempts removes all failed HTLC attempts from the database for
// the specified payment, while preserving the payment record itself and any
// successful or in-flight attempts.