paymentsdb: implement DeleteFailedAttempts for sql backend

This commit is contained in:
ziggie committed 2026-02-25 08:02:09 +01:00
1 parent 6faf68c5fa
commit 3012eb2ca0
4 files changed
+126

No files matched your search

+2
View File
@@ -291,6 +291,8 @@ func (p *KVStore) InitPayment(paymentHash lntypes.Hash,
// DeleteFailedAttempts deletes all failed htlcs for a payment if configured
// by the KVStore db.
func (p *KVStore) DeleteFailedAttempts(hash lntypes.Hash) error {
// TODO(ziggie): Refactor to not mix application logic with database
// logic. This decision should be made in the application layer.
if !p.keepFailedPaymentAttempts {
const failedHtlcsOnly = true
err := p.DeletePayment(hash, failedHtlcsOnly)
+120
View File
@@ -712,3 +712,123 @@ func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) {
return mpPayment, 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.
//
// The method performs the following validations before deletion:
// - StatusInitiated: Can delete failed attempts
// - StatusInFlight: Cannot delete, returns ErrPaymentInFlight (active HTLCs
// still on the network)
// - StatusSucceeded: Can delete failed attempts (payment completed)
// - StatusFailed: Can delete failed attempts (payment permanently failed)
//
// If the keepFailedPaymentAttempts configuration flag is enabled, this method
// returns immediately without deleting anything, allowing failed attempts to
// be retained for debugging or auditing purposes.
//
// This method is idempotent - calling it multiple times on the same payment
// has no adverse effects.
//
// This method is part of the PaymentControl interface, which is embedded in
// the PaymentWriter interface and ultimately the DB interface. It represents
// the final step (step 5) in the payment lifecycle control flow and should be
// called after a payment reaches a terminal state (succeeded or permanently
// failed) to clean up historical failed attempts.
func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error {
ctx := context.TODO()
// In case we are configured to keep failed payment attempts, we exit
// early.
//
// TODO(ziggie): Refactor to not mix application logic with database
// logic. This decision should be made in the application layer.
if s.keepFailedPaymentAttempts {
return nil
}
err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
if err != nil {
return fmt.Errorf("failed to fetch payment: %w", err)
}
paymentStatus, err := computePaymentStatusFromDB(
ctx, db, dbPayment,
)
if err != nil {
return fmt.Errorf("failed to compute payment "+
"status: %w", err)
}
if err := paymentStatus.removable(); err != nil {
return fmt.Errorf("cannot delete failed "+
"attempts for payment %v: %w", paymentHash, err)
}
// Then we delete the failed attempts for this payment.
return db.DeleteFailedAttempts(ctx, dbPayment.Payment.ID)
}, sqldb.NoOpReset)
if err != nil {
return fmt.Errorf("failed to delete failed attempts for "+
"payment %v: %w", paymentHash, err)
}
return nil
}
// computePaymentStatusFromDB computes the payment status by fetching minimal
// data from the database. This is a lightweight query optimized for SQL that
// doesn't load route data, making it significantly more efficient than
// FetchPayment when only the status is needed.
func computePaymentStatusFromDB(ctx context.Context, db SQLQueries,
dbPayment sqlc.PaymentAndIntent) (PaymentStatus, error) {
payment := dbPayment.GetPayment()
resolutionTypes, err := db.FetchHtlcAttemptResolutionsForPayment(
ctx, payment.ID,
)
if err != nil {
return 0, fmt.Errorf("failed to fetch htlc resolutions: %w",
err)
}
// Build minimal HTLCAttempt slice with only resolution info.
htlcs := make([]HTLCAttempt, len(resolutionTypes))
for i, resType := range resolutionTypes {
if !resType.Valid {
// NULL resolution_type means in-flight (no Settle, no
// Failure).
continue
}
switch HTLCAttemptResolutionType(resType.Int32) {
case HTLCAttemptResolutionSettled:
// Mark as settled (preimage details not needed for
// status).
htlcs[i].Settle = &HTLCSettleInfo{}
case HTLCAttemptResolutionFailed:
// Mark as failed (failure details not needed for
// status).
htlcs[i].Failure = &HTLCFailInfo{}
}
}
// Convert fail reason to FailureReason pointer.
var failureReason *FailureReason
if payment.FailReason.Valid {
reason := FailureReason(payment.FailReason.Int32)
failureReason = &reason
}
// Use the existing status decision logic.
status, err := decidePaymentStatus(htlcs, failureReason)
if err != nil {
return 0, fmt.Errorf("failed to decide payment status: %w", err)
}
return status, nil
}
+2
View File
@@ -29,6 +29,8 @@ DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN (
)
`
// Delete all failed HTLC attempts for the given payment. Resolution type 2
// indicates a failed attempt.
func (q *Queries) DeleteFailedAttempts(ctx context.Context, paymentID int64) error {
_, err := q.db.ExecContext(ctx, deleteFailedAttempts, paymentID)
return err
+2
View File
@@ -22,6 +22,8 @@ type Querier interface {
DeleteChannelPolicyExtraTypes(ctx context.Context, channelPolicyID int64) error
DeleteChannels(ctx context.Context, ids []int64) error
DeleteExtraNodeType(ctx context.Context, arg DeleteExtraNodeTypeParams) error
// Delete all failed HTLC attempts for the given payment. Resolution type 2
// indicates a failed attempt.
DeleteFailedAttempts(ctx context.Context, paymentID int64) error
DeleteInvoice(ctx context.Context, arg DeleteInvoiceParams) (sql.Result, error)
DeleteNode(ctx context.Context, id int64) error