-
-
Notifications
You must be signed in to change notification settings - Fork 47
feat: Automatically retry failed webhook deliveries #745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
030ae4e
2db2bb7
0328f26
6252588
758d127
f897759
059eea7
e5add06
a02ce90
bbe3117
940ee81
2cf243b
bb819cd
4e76ff2
67cc182
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { Octokit } from '@octokit/rest'; | ||
import { getAppOctokit, redeliver } from './lambda-github'; | ||
|
||
/** | ||
* Get webhook delivery failures since the last processed delivery ID. | ||
* | ||
* @internal | ||
*/ | ||
async function newDeliveryFailures(octokit: Octokit, sinceId: number) { | ||
const deliveries: Map<string, { id: number; deliveredAt: Date; redelivery: boolean }> = new Map(); | ||
const successfulDeliveries: Set<string> = new Set(); | ||
const timeLimitMs = 1000 * 60 * 30; // don't look at deliveries over 30 minutes old | ||
let lastId = 0; | ||
|
||
for await (const response of octokit.paginate.iterator('GET /app/hook/deliveries')) { | ||
if (response.status !== 200) { | ||
throw new Error('Failed to fetch webhook deliveries'); | ||
} | ||
|
||
for (const delivery of response.data) { | ||
const deliveredAt = new Date(delivery.delivered_at); | ||
const success = delivery.status === 'OK'; | ||
|
||
if (delivery.id < sinceId) { | ||
// stop processing if we reach the last processed delivery ID | ||
console.debug({ | ||
notice: 'Reached last processed delivery ID', | ||
sinceId: sinceId, | ||
deliveryId: delivery.id, | ||
guid: delivery.guid, | ||
}); | ||
return { deliveries, lastId }; | ||
} | ||
|
||
lastId = Math.max(lastId, delivery.id); | ||
|
||
if (deliveredAt.getTime() < Date.now() - timeLimitMs) { | ||
// stop processing if the delivery is too old (for first iteration and performance of further iterations) | ||
console.debug({ | ||
notice: 'Stopping at old delivery', | ||
deliveryId: delivery.id, | ||
guid: delivery.guid, | ||
deliveredAt: deliveredAt, | ||
}); | ||
return { deliveries, lastId }; | ||
} | ||
|
||
console.debug({ | ||
notice: 'Processing webhook delivery', | ||
deliveryId: delivery.id, | ||
guid: delivery.guid, | ||
status: delivery.status, | ||
deliveredAt: delivery.delivered_at, | ||
redelivery: delivery.redelivery, | ||
}); | ||
|
||
if (success) { | ||
successfulDeliveries.add(delivery.guid); | ||
continue; | ||
} | ||
|
||
if (successfulDeliveries.has(delivery.guid)) { | ||
// do not redeliver deliveries that were already successful | ||
continue; | ||
} | ||
|
||
deliveries.set(delivery.guid, { id: delivery.id, deliveredAt, redelivery: delivery.redelivery }); | ||
} | ||
} | ||
|
||
console.debug({ | ||
notice: 'No more webhook deliveries to process', | ||
deliveryId: 'DONE', | ||
guid: 'DONE', | ||
deliveredAt: 'DONE', | ||
}); | ||
|
||
return { deliveries, lastId }; | ||
} | ||
|
||
let lastDeliveryIdProcessed = 0; | ||
const failures: Map<string, { id: number; firstDeliveredAt: Date }> = new Map(); | ||
|
||
export async function handler() { | ||
const octokit = await getAppOctokit(); | ||
if (!octokit) { | ||
console.info({ | ||
notice: 'Skipping webhook redelivery', | ||
reason: 'App installation might not be configured or the app is not installed.', | ||
}); | ||
return; | ||
} | ||
|
||
// fetch deliveries since the last processed delivery ID | ||
// for any failures: | ||
// 1. if this is not a redelivery, save the delivery ID and time, and finally retry | ||
// 2. if this is a redelivery, check if the original delivery is still within the time limit and retry if it is | ||
const { deliveries, lastId } = await newDeliveryFailures(octokit, lastDeliveryIdProcessed); | ||
lastDeliveryIdProcessed = Math.max(lastDeliveryIdProcessed, lastId); | ||
const timeLimitMs = 1000 * 60 * 60 * 3; // retry for up to 3 hours | ||
for (const [guid, details] of deliveries) { | ||
if (!details.redelivery) { | ||
failures.set(guid, { id: details.id, firstDeliveredAt: details.deliveredAt }); | ||
console.log({ | ||
notice: 'Redelivering failed delivery', | ||
deliveryId: details.id, | ||
guid: guid, | ||
firstDeliveredAt: details.deliveredAt, | ||
}); | ||
await redeliver(octokit, details.id); | ||
} else { | ||
// if this is a redelivery, check if the original delivery is still within the time limit | ||
const originalFailure = failures.get(guid); | ||
if (originalFailure) { | ||
if (new Date().getTime() - originalFailure.firstDeliveredAt.getTime() < timeLimitMs) { | ||
console.log({ | ||
notice: 'Redelivering failed delivery', | ||
deliveryId: details.id, | ||
guid: guid, | ||
firstDeliveredAt: originalFailure.firstDeliveredAt, | ||
}); | ||
await redeliver(octokit, details.id); | ||
} else { | ||
failures.delete(guid); // no need to keep track of this anymore | ||
console.log({ | ||
notice: 'Skipping redelivery of old failed delivery', | ||
deliveryId: details.id, | ||
guid: guid, | ||
firstDeliveredAt: originalFailure?.firstDeliveredAt, | ||
}); | ||
} | ||
} else { | ||
console.log({ | ||
notice: 'Skipping redelivery of old failed delivery', | ||
deliveryId: details.id, | ||
guid: guid, | ||
}); | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.