Skip to content

Commit

Permalink
Use expiring lock in retry mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed Feb 4, 2025
1 parent 2bab33f commit 95e4000
Show file tree
Hide file tree
Showing 7 changed files with 23 additions and 35 deletions.
9 changes: 3 additions & 6 deletions api/paidAction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ stateDiagram-v2
PENDING --> FAILED
PAID --> [*]
CANCELING --> FAILED
FAILED --> RETRY_PENDING
FAILED --> RETRYING
FAILED --> [*]
RETRY_PENDING --> RETRYING
RETRYING --> [*]
[*] --> PENDING_HELD
PENDING_HELD --> HELD
Expand Down Expand Up @@ -60,9 +59,8 @@ stateDiagram-v2
PENDING --> FAILED
PAID --> [*]
CANCELING --> FAILED
FAILED --> RETRY_PENDING
FAILED --> RETRYING
FAILED --> [*]
RETRY_PENDING --> RETRYING
RETRYING --> [*]
```
</details>
Expand Down Expand Up @@ -123,9 +121,8 @@ This works by requesting an invoice from the recipient's wallet and reusing the
stateDiagram-v2
PAID --> [*]
CANCELING --> FAILED
FAILED --> RETRY_PENDING
FAILED --> RETRYING
FAILED --> [*]
RETRY_PENDING --> RETRYING
RETRYING --> [*]
[*] --> PENDING_HELD
PENDING_HELD --> FORWARDING
Expand Down
2 changes: 1 addition & 1 deletion api/paidAction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
await tx.invoice.update({
where: {
id: failedInvoice.id,
actionState: 'RETRY_PENDING'
actionState: 'FAILED'
},
data: {
actionState: 'RETRYING'
Expand Down
35 changes: 13 additions & 22 deletions api/resolvers/paidAction.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { retryPaidAction } from '../paidAction'
import { USER_ID, WALLET_MAX_RETRIES } from '@/lib/constants'
import { USER_ID, WALLET_MAX_RETRIES, WALLET_RETRY_TIMEOUT_MS } from '@/lib/constants'

function paidActionType (actionType) {
switch (actionType) {
Expand Down Expand Up @@ -55,18 +55,18 @@ export default {
throw new Error('You must be logged in')
}

// make sure only one client at a time can retry by immediately transitioning to an intermediate state
// make sure only one client at a time can retry by acquiring a lock that expires
const [invoice] = await models.$queryRaw`
UPDATE "Invoice"
SET "actionState" = 'RETRY_PENDING'
WHERE id in (
SELECT id FROM "Invoice"
WHERE id = ${invoiceId} AND "userId" = ${me.id} AND "actionState" = 'FAILED'
FOR UPDATE
)
SET "retryPendingSince" = CURRENT_TIMESTAMP
WHERE
id = ${invoiceId} AND
"userId" = ${me.id} AND
"actionState" = 'FAILED' AND
("retryPendingSince" IS NULL OR "retryPendingSince" < now() - ${`${WALLET_RETRY_TIMEOUT_MS} milliseconds`}::interval)
RETURNING *`
if (!invoice) {
throw new Error('Invoice not found')
throw new Error('Invoice not found or retry pending')
}

// do we want to retry a payment from the beginning with all sender and receiver wallets?
Expand All @@ -75,19 +75,10 @@ export default {
throw new Error('Payment has been retried too many times')
}

try {
const result = await retryPaidAction(invoice.actionType, { invoice }, { paymentAttempt, models, me, lnd })
return {
...result,
type: paidActionType(invoice.actionType)
}
} catch (err) {
// revert state transition to allow new retry for this invoice
await models.invoice.update({
where: { id: invoiceId, actionState: 'RETRY_PENDING' },
data: { actionState: 'FAILED' }
})
throw err
const result = await retryPaidAction(invoice.actionType, { invoice }, { paymentAttempt, models, me, lnd })
return {
...result,
type: paidActionType(invoice.actionType)
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,5 +205,7 @@ export const WALLET_RETRY_AFTER_MS = 60_000 // 1 minute
export const WALLET_RETRY_BEFORE_MS = 86_400_000 // 24 hours
// we want to attempt a payment three times so we retry two times
export const WALLET_MAX_RETRIES = 2
// when a pending retry for an invoice should be considered expired and can be attempted again
export const WALLET_RETRY_TIMEOUT_MS = 60_000 // 1 minute

export const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
-- AlterTable
ALTER TABLE "Invoice" ADD COLUMN "paymentAttempt" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Invoice" ADD COLUMN "retryPendingSince" TIMESTAMP(3);
CREATE INDEX "Invoice_cancelledAt_idx" ON "Invoice"("cancelledAt");

-- AlterEnum
ALTER TYPE "InvoiceActionState" ADD VALUE 'RETRY_PENDING';
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,6 @@ enum InvoiceActionState {
FORWARDING
FORWARDED
FAILED_FORWARD
RETRY_PENDING
RETRYING
CANCELING
}
Expand Down Expand Up @@ -928,6 +927,7 @@ model Invoice {
cancelledAt DateTime?
userCancel Boolean?
paymentAttempt Int @default(0)
retryPendingSince DateTime?
msatsRequested BigInt
msatsReceived BigInt?
desc String?
Expand Down
4 changes: 2 additions & 2 deletions wallets/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export async function createWrappedInvoice (userId,

export async function getInvoiceableWallets (userId, { paymentAttempt, predecessorId, models }) {
// filter out all wallets that have already been tried by recursively following the retry chain of predecessor invoices.
// the current predecessor invoice is in state 'RETRY_PENDING' and not in state 'RETRYING'
// the current predecessor invoice is in state 'FAILED' and not in state 'RETRYING'
// because we are currently retrying it so it has not been updated yet.
// if predecessorId is not provided, the subquery will be empty and thus no wallets are filtered out.
const wallets = await models.$queryRaw`
Expand All @@ -135,7 +135,7 @@ export async function getInvoiceableWallets (userId, { paymentAttempt, predecess
-- this failed invoice will be used to start the recursion
SELECT "Invoice"."id", "Invoice"."predecessorId"
FROM "Invoice"
WHERE "Invoice"."id" = ${predecessorId} AND "Invoice"."actionState" = 'RETRY_PENDING'
WHERE "Invoice"."id" = ${predecessorId} AND "Invoice"."actionState" = 'FAILED'
UNION ALL
Expand Down

0 comments on commit 95e4000

Please sign in to comment.