Skip to content
Draft
26 changes: 20 additions & 6 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,16 @@ export class App {
process.nextTick(() => this.processWalletAddress())
}
for (let i = 0; i < this.config.outgoingPaymentWorkers; i++) {
process.nextTick(() => this.processOutgoingPayment())
setTimeout(
() => this.processOutgoingPayment(),
this.config.outgoingPaymentProcessingIntervalMs
)
}
for (let i = 0; i < this.config.incomingPaymentWorkers; i++) {
process.nextTick(() => this.processIncomingPayment())
setTimeout(
() => this.processIncomingPayment(),
this.config.incomingPaymentProcessingIntervalMs
)
}
for (let i = 0; i < this.config.webhookWorkers; i++) {
process.nextTick(() => this.processWebhook())
Expand Down Expand Up @@ -888,11 +894,15 @@ export class App {
return true
})
.then((hasMoreWork) => {
if (hasMoreWork) process.nextTick(() => this.processOutgoingPayment())
if (hasMoreWork)
setTimeout(
() => this.processOutgoingPayment(),
this.config.outgoingPaymentProcessingIntervalMs
)
else
setTimeout(
() => this.processOutgoingPayment(),
this.config.outgoingPaymentWorkerIdle
this.config.outgoingPaymentProcessingIntervalMs
).unref()
})
}
Expand Down Expand Up @@ -938,11 +948,15 @@ export class App {
return true
})
.then((hasMoreWork) => {
if (hasMoreWork) process.nextTick(() => this.processIncomingPayment())
if (hasMoreWork)
setTimeout(
() => this.processIncomingPayment(),
this.config.incomingPaymentProcessingIntervalMs
)
else
setTimeout(
() => this.processIncomingPayment(),
this.config.incomingPaymentWorkerIdle
this.config.incomingPaymentProcessingIntervalMs
).unref()
})
}
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,19 @@ export const Config = {

outgoingPaymentWorkers: envInt('OUTGOING_PAYMENT_WORKERS', 1),
outgoingPaymentWorkerIdle: envInt('OUTGOING_PAYMENT_WORKER_IDLE', 10), // milliseconds
outgoingPaymentBatchSize: envInt('OUTGOING_PAYMENT_WORKER_BATCH_SIZE', 250),
outgoingPaymentProcessingIntervalMs: envInt(
'OUTGOING_PAYMENT_PROCESSING_INTERVAL_MS',
200
), // milliseconds

incomingPaymentWorkers: envInt('INCOMING_PAYMENT_WORKERS', 1),
incomingPaymentWorkerIdle: envInt('INCOMING_PAYMENT_WORKER_IDLE', 200), // milliseconds
incomingPaymentBatchSize: envInt('INCOMING_PAYMENT_WORKER_BATCH_SIZE', 250),
incomingPaymentProcessingIntervalMs: envInt(
'INCOMING_PAYMENT_PROCESSING_INTERVAL_MS',
200
), // milliseconds
pollIncomingPaymentCreatedWebhook: envBool(
'POLL_INCOMING_PAYMENT_CREATED_WEBHOOK',
false
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,11 @@ export const start = async (
logger.info(`Connector listening on ${config.connectorPort}`)
logger.info('🐒 has 🚀. Get ready for 🍌🍌🍌🍌🍌')

logger.info(
`Running the Rafiki WW 2026 build with an outgoing payment batch size of ${config.outgoingPaymentBatchSize}.`
)
logger.info(`Happy Testing - Nathan 🫡`)

if (config.enableAutoPeering) {
await app.startAutoPeeringServer(config.autoPeeringServerPort)
logger.info(
Expand Down
20 changes: 10 additions & 10 deletions packages/backend/src/open_payments/payment/incoming/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ describe('Incoming Payment Service', (): void => {

jest.useFakeTimers()
jest.setSystemTime(incomingPayment.expiresAt)
await expect(incomingPaymentService.processNext()).resolves.toBe(
await expect(incomingPaymentService.processNext()).resolves.toContain(
incomingPayment.id
)
await expect(
Expand Down Expand Up @@ -803,7 +803,7 @@ describe('Incoming Payment Service', (): void => {
})
jest.useFakeTimers()
jest.setSystemTime(incomingPayment.expiresAt)
await expect(incomingPaymentService.processNext()).resolves.toBe(
await expect(incomingPaymentService.processNext()).resolves.toContain(
incomingPayment.id
)

Expand Down Expand Up @@ -886,9 +886,9 @@ describe('Incoming Payment Service', (): void => {
if (eventType === IncomingPaymentEventType.IncomingPaymentExpired) {
jest.useFakeTimers()
jest.setSystemTime(incomingPayment.expiresAt)
await expect(incomingPaymentService.processNext()).resolves.toBe(
incomingPayment.id
)
await expect(
incomingPaymentService.processNext()
).resolves.toContain(incomingPayment.id)
} else {
await incomingPayment.onCredit({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down Expand Up @@ -923,7 +923,7 @@ describe('Incoming Payment Service', (): void => {
assert.ok(incomingPayment.processAt)
jest.useFakeTimers()
jest.setSystemTime(incomingPayment.processAt)
await expect(incomingPaymentService.processNext()).resolves.toBe(
await expect(incomingPaymentService.processNext()).resolves.toContain(
incomingPayment.id
)
const events = await IncomingPaymentEvent.query(knex)
Expand Down Expand Up @@ -974,9 +974,9 @@ describe('Incoming Payment Service', (): void => {
assert.ok(incomingPayment.processAt)
jest.useFakeTimers()
jest.setSystemTime(incomingPayment.processAt)
await expect(incomingPaymentService.processNext()).resolves.toBe(
incomingPayment.id
)
await expect(
incomingPaymentService.processNext()
).resolves.toContain(incomingPayment.id)
const events = await IncomingPaymentEvent.query(knex)
.where({
incomingPaymentId: incomingPayment.id,
Expand Down Expand Up @@ -1127,7 +1127,7 @@ describe('Incoming Payment Service', (): void => {
const future = new Date(Date.now() + 40_000)
jest.useFakeTimers()
jest.setSystemTime(future)
await expect(incomingPaymentService.processNext()).resolves.toBe(
await expect(incomingPaymentService.processNext()).resolves.toContain(
incomingPayment.id
)
await expect(
Expand Down
55 changes: 29 additions & 26 deletions packages/backend/src/open_payments/payment/incoming/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export interface IncomingPaymentService
id: string,
tenantId?: string
): Promise<IncomingPayment | IncomingPaymentError>
processNext(): Promise<string | undefined>
processNext(): Promise<string[] | undefined>
update(
options: UpdateOptions
): Promise<IncomingPayment | IncomingPaymentError>
Expand Down Expand Up @@ -313,43 +313,46 @@ async function getApprovedOrCanceledIncomingPayment(
// Returns the id of the processed incoming payment (if any).
async function processNextIncomingPayment(
deps_: ServiceDependencies
): Promise<string | undefined> {
): Promise<string[] | undefined> {
return deps_.knex.transaction(async (trx) => {
const now = new Date(Date.now()).toISOString()
const incomingPayments = await IncomingPayment.query(trx)
.limit(1)
.limit(deps_.config.incomingPaymentBatchSize)
// Ensure the incoming payments cannot be processed concurrently by multiple workers.
.forUpdate()
// If an incoming payment is locked, don't wait — just come back for it later.
.skipLocked()
.where('processAt', '<=', now)

const incomingPayment = incomingPayments[0]
if (!incomingPayment) return
if (incomingPayments.length === 0) return

const asset = await deps_.assetService.get(incomingPayment.assetId)
if (asset) incomingPayment.asset = asset

incomingPayment.walletAddress = await deps_.walletAddressService.get(
incomingPayment.walletAddressId
)
const processIncoming = async (incomingPayment: IncomingPayment) => {
const asset = await deps_.assetService.get(incomingPayment.assetId)
if (asset) incomingPayment.asset = asset
incomingPayment.walletAddress = await deps_.walletAddressService.get(
incomingPayment.walletAddressId
)

const deps = {
...deps_,
knex: trx,
logger: deps_.logger.child({
incomingPayment: incomingPayment.id
})
}
if (
incomingPayment.state === IncomingPaymentState.Expired ||
incomingPayment.state === IncomingPaymentState.Completed
) {
await handleDeactivated(deps, incomingPayment)
} else {
await handleExpired(deps, incomingPayment)
const deps = {
...deps_,
knex: trx,
logger: deps_.logger.child({
incomingPayment: incomingPayment.id
})
}
if (
incomingPayment.state === IncomingPaymentState.Expired ||
incomingPayment.state === IncomingPaymentState.Completed
) {
await handleDeactivated(deps, incomingPayment)
} else {
await handleExpired(deps, incomingPayment)
}
return incomingPayment.id
}
return incomingPayment.id

const promises = incomingPayments.map(processIncoming)
return Promise.all(promises)
})
}

Expand Down
Loading
Loading