mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
perf: Avoid empty webhook processor work via cached subscription lookup (#12593)
* Avoid empty webhook processor work via cached subscription lookup
WebhookProcessor ran for every event but most teams have no matching
webhook subscription, costing an empty processor job and a database query
per event.
Cache a team's enabled subscriptions ({ id, events }) in Redis, invalidated
by model lifecycle hooks, and add an optional BaseProcessor.shouldQueue hook
consulted by the global event queue so the webhook processor only enqueues a
job when a matching subscription exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feedback
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -86,4 +86,51 @@ describe("WebhookProcessor", () => {
|
||||
subscriptionId: subscriptionTwo.id,
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldQueue", () => {
|
||||
it("returns true when a matching subscription exists", async () => {
|
||||
const subscription = await buildWebhookSubscription({
|
||||
url: "http://example.com",
|
||||
events: ["users"],
|
||||
});
|
||||
const event: UserEvent = {
|
||||
name: "users.signin",
|
||||
userId: subscription.createdById,
|
||||
teamId: subscription.teamId,
|
||||
actorId: subscription.createdById,
|
||||
ip,
|
||||
};
|
||||
|
||||
expect(await WebhookProcessor.shouldQueue(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when no subscription matches the event", async () => {
|
||||
const subscription = await buildWebhookSubscription({
|
||||
url: "http://example.com",
|
||||
events: ["documents.create"],
|
||||
});
|
||||
const event: UserEvent = {
|
||||
name: "users.signin",
|
||||
userId: subscription.createdById,
|
||||
teamId: subscription.teamId,
|
||||
actorId: subscription.createdById,
|
||||
ip,
|
||||
};
|
||||
|
||||
expect(await WebhookProcessor.shouldQueue(event)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when the team has no subscriptions", async () => {
|
||||
const user = await buildUser();
|
||||
const event: UserEvent = {
|
||||
name: "users.signin",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
ip,
|
||||
};
|
||||
|
||||
expect(await WebhookProcessor.shouldQueue(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,20 +6,39 @@ import DeliverWebhookTask from "../tasks/DeliverWebhookTask";
|
||||
export default class WebhookProcessor extends BaseProcessor {
|
||||
static applicableEvents: ["*"] = ["*"];
|
||||
|
||||
/**
|
||||
* Only queue an event when the team has an enabled webhook subscription that
|
||||
* matches it. The vast majority of events belong to teams with no applicable
|
||||
* subscriptions, so this avoids creating and running an empty job for them.
|
||||
*
|
||||
* @param event The event about to be queued.
|
||||
* @returns true if a matching subscription exists.
|
||||
*/
|
||||
static async shouldQueue(event: Event): Promise<boolean> {
|
||||
if (!event.teamId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subscriptions = await WebhookSubscription.findEnabledByTeamId(
|
||||
event.teamId
|
||||
);
|
||||
|
||||
return subscriptions.some((subscription) =>
|
||||
WebhookSubscription.matchEvent(subscription.events, event.name)
|
||||
);
|
||||
}
|
||||
|
||||
async perform(event: Event) {
|
||||
if (!event.teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webhookSubscriptions = await WebhookSubscription.findAll({
|
||||
where: {
|
||||
enabled: true,
|
||||
teamId: event.teamId,
|
||||
},
|
||||
});
|
||||
const subscriptions = await WebhookSubscription.findEnabledByTeamId(
|
||||
event.teamId
|
||||
);
|
||||
|
||||
const applicableSubscriptions = webhookSubscriptions.filter((webhook) =>
|
||||
webhook.validForEvent(event)
|
||||
const applicableSubscriptions = subscriptions.filter((subscription) =>
|
||||
WebhookSubscription.matchEvent(subscription.events, event.name)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
|
||||
Reference in New Issue
Block a user