Skip to content

Commit 25684f9

Browse files
montocodermohammed-bahumaishCarinaWolli
authored
fix: meeting ended trigger for webhooks and zapier sometimes not working (#10946)
Co-authored-by: mohammed gehad <[email protected]> Co-authored-by: Monto <[email protected]> Co-authored-by: Carina Wollendorfer <[email protected]>
1 parent bc9aeef commit 25684f9

File tree

12 files changed

+173
-42
lines changed

12 files changed

+173
-42
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Cron - webhookTriggers
2+
3+
on:
4+
# "Scheduled workflows run on the latest commit on the default or base branch."
5+
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
6+
schedule:
7+
# Runs “every 5 minutes” (see https://crontab.guru)
8+
- cron: "*/5 * * * *"
9+
jobs:
10+
cron-webhookTriggers:
11+
env:
12+
APP_URL: ${{ secrets.APP_URL }}
13+
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: cURL request
17+
if: ${{ env.APP_URL && env.CRON_API_KEY }}
18+
run: |
19+
curl ${{ secrets.APP_URL }}/api/cron/webhookTriggers \
20+
-X POST \
21+
-H 'content-type: application/json' \
22+
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
23+
--fail
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "@calcom/features/webhooks/lib/cron";

packages/app-store/zapier/api/subscriptions/addSubscription.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { Prisma } from "@prisma/client";
22
import type { NextApiRequest, NextApiResponse } from "next";
33
import { v4 } from "uuid";
44

5-
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
65
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
6+
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
77
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
88
import prisma from "@calcom/prisma";
99
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import appStore from "@calcom/app-store";
55
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
66
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
77
import { DailyLocationType } from "@calcom/app-store/locations";
8-
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
98
import { deleteMeeting, updateMeeting } from "@calcom/core/videoClient";
109
import dayjs from "@calcom/dayjs";
1110
import { sendCancelledEmails, sendCancelledSeatEmails } from "@calcom/emails";
@@ -16,6 +15,7 @@ import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/remind
1615
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
1716
import { deleteScheduledWhatsappReminder } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
1817
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
18+
import { cancelScheduledJobs } from "@calcom/features/webhooks/lib/scheduleTrigger";
1919
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
2020
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
2121
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";

packages/features/bookings/lib/handleConfirmation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
22

3-
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
43
import type { EventManagerUser } from "@calcom/core/EventManager";
54
import EventManager from "@calcom/core/EventManager";
65
import { sendScheduledEmails } from "@calcom/emails";
76
import { isEventTypeOwnerKYCVerified } from "@calcom/features/ee/workflows/lib/isEventTypeOwnerKYCVerified";
87
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
98
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
9+
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
1010
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
1111
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
1212
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
} from "@calcom/app-store/locations";
2020
import type { EventTypeAppsList } from "@calcom/app-store/utils";
2121
import { getAppFromSlug } from "@calcom/app-store/utils";
22-
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
2322
import EventManager from "@calcom/core/EventManager";
2423
import { getEventName } from "@calcom/core/event";
2524
import { getUserAvailability } from "@calcom/core/getUserAvailability";
@@ -48,6 +47,7 @@ import {
4847
import { getFullName } from "@calcom/features/form-builder/utils";
4948
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
5049
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
50+
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
5151
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
5252
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
5353
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* Cron job for scheduled webhook events triggers */
2+
import type { NextApiRequest, NextApiResponse } from "next";
3+
4+
import dayjs from "@calcom/dayjs";
5+
import { defaultHandler } from "@calcom/lib/server";
6+
import prisma from "@calcom/prisma";
7+
8+
async function handler(req: NextApiRequest, res: NextApiResponse) {
9+
const apiKey = req.headers.authorization || req.query.apiKey;
10+
if (process.env.CRON_API_KEY !== apiKey) {
11+
res.status(401).json({ message: "Not authenticated" });
12+
return;
13+
}
14+
15+
// get jobs that should be run
16+
const jobsToRun = await prisma.webhookScheduledTriggers.findMany({
17+
where: {
18+
startAfter: {
19+
lte: dayjs().toISOString(),
20+
},
21+
},
22+
});
23+
24+
// run jobs
25+
for (const job of jobsToRun) {
26+
try {
27+
await fetch(job.subscriberUrl, {
28+
method: "POST",
29+
body: job.payload,
30+
});
31+
} catch (error) {
32+
console.log(`Error running webhook trigger (retry count: ${job.retryCount}): ${error}`);
33+
34+
// if job fails, retry again for 5 times.
35+
if (job.retryCount < 5) {
36+
await prisma.webhookScheduledTriggers.update({
37+
where: {
38+
id: job.id,
39+
},
40+
data: {
41+
retryCount: {
42+
increment: 1,
43+
},
44+
startAfter: dayjs()
45+
.add(5 * (job.retryCount + 1), "minutes")
46+
.toISOString(),
47+
},
48+
});
49+
return res.json({ ok: false });
50+
}
51+
}
52+
53+
const parsedJobPayload = JSON.parse(job.payload) as {
54+
id: number; // booking id
55+
endTime: string;
56+
scheduledJobs: string[];
57+
triggerEvent: string;
58+
};
59+
60+
// clean finished job
61+
await prisma.webhookScheduledTriggers.delete({
62+
where: {
63+
id: job.id,
64+
},
65+
});
66+
67+
const booking = await prisma.booking.findUnique({
68+
where: { id: parsedJobPayload.id },
69+
select: { id: true, scheduledJobs: true },
70+
});
71+
if (!booking) {
72+
console.log("Error finding booking in webhook trigger:", parsedJobPayload);
73+
return res.json({ ok: false });
74+
}
75+
76+
//remove scheduled job from bookings once triggered
77+
const updatedScheduledJobs = booking.scheduledJobs.filter((scheduledJob) => {
78+
return scheduledJob !== job.jobName;
79+
});
80+
81+
await prisma.booking.update({
82+
where: {
83+
id: booking.id,
84+
},
85+
data: {
86+
scheduledJobs: updatedScheduledJobs,
87+
},
88+
});
89+
}
90+
91+
res.json({ ok: true });
92+
}
93+
94+
export default defaultHandler({
95+
POST: Promise.resolve({ default: handler }),
96+
});

packages/app-store/zapier/lib/nodeScheduler.ts renamed to packages/features/webhooks/lib/scheduleTrigger.ts

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import schedule from "node-schedule";
2-
31
import prisma from "@calcom/prisma";
42
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
53

@@ -9,45 +7,32 @@ export async function scheduleTrigger(
97
subscriber: { id: string; appId: string | null }
108
) {
119
try {
12-
//schedule job to call subscriber url at the end of meeting
13-
// FIXME: in-process scheduling - job will vanish on server crash / restart
14-
const job = schedule.scheduleJob(
15-
`${subscriber.appId}_${subscriber.id}`,
16-
booking.endTime,
17-
async function () {
18-
const body = JSON.stringify({ triggerEvent: WebhookTriggerEvents.MEETING_ENDED, ...booking });
19-
await fetch(subscriberUrl, {
20-
method: "POST",
21-
body,
22-
});
10+
const payload = JSON.stringify({ triggerEvent: WebhookTriggerEvents.MEETING_ENDED, ...booking });
11+
const jobName = `${subscriber.appId}_${subscriber.id}`;
2312

24-
//remove scheduled job from bookings once triggered
25-
const updatedScheduledJobs = booking.scheduledJobs.filter((scheduledJob) => {
26-
return scheduledJob !== `${subscriber.appId}_${subscriber.id}`;
27-
});
28-
29-
await prisma.booking.update({
30-
where: {
31-
id: booking.id,
32-
},
33-
data: {
34-
scheduledJobs: updatedScheduledJobs,
35-
},
36-
});
37-
}
38-
);
13+
// add scheduled job to database
14+
const createTrigger = prisma.webhookScheduledTriggers.create({
15+
data: {
16+
jobName,
17+
payload,
18+
startAfter: booking.endTime,
19+
subscriberUrl,
20+
},
21+
});
3922

4023
//add scheduled job name to booking
41-
await prisma.booking.update({
24+
const updateBooking = prisma.booking.update({
4225
where: {
4326
id: booking.id,
4427
},
4528
data: {
4629
scheduledJobs: {
47-
push: job.name,
30+
push: jobName,
4831
},
4932
},
5033
});
34+
35+
await prisma.$transaction([createTrigger, updateBooking]);
5136
} catch (error) {
5237
console.error("Error cancelling scheduled jobs", error);
5338
}
@@ -64,16 +49,20 @@ export async function cancelScheduledJobs(
6449
const promises = booking.scheduledJobs.map(async (scheduledJob) => {
6550
if (appId) {
6651
if (scheduledJob.startsWith(appId)) {
67-
if (schedule.scheduledJobs[scheduledJob]) {
68-
schedule.scheduledJobs[scheduledJob].cancel();
69-
}
52+
await prisma.webhookScheduledTriggers.deleteMany({
53+
where: {
54+
jobName: scheduledJob,
55+
},
56+
});
7057
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
7158
}
7259
} else {
7360
//if no specific appId given, delete all scheduled jobs of booking
74-
if (schedule.scheduledJobs[scheduledJob]) {
75-
schedule.scheduledJobs[scheduledJob].cancel();
76-
}
61+
await prisma.webhookScheduledTriggers.deleteMany({
62+
where: {
63+
jobName: scheduledJob,
64+
},
65+
});
7766
scheduledJobs = [];
7867
}
7968

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- CreateTable
2+
CREATE TABLE "WebhookScheduledTriggers" (
3+
"id" SERIAL NOT NULL,
4+
"jobName" TEXT NOT NULL,
5+
"subscriberUrl" TEXT NOT NULL,
6+
"payload" TEXT NOT NULL,
7+
"startAfter" TIMESTAMP(3) NOT NULL,
8+
"retryCount" INTEGER NOT NULL DEFAULT 0,
9+
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
10+
11+
CONSTRAINT "WebhookScheduledTriggers_pkey" PRIMARY KEY ("id")
12+
);

packages/prisma/schema.prisma

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,16 @@ model WorkflowReminder {
807807
@@index([seatReferenceId])
808808
}
809809

810+
model WebhookScheduledTriggers {
811+
id Int @id @default(autoincrement())
812+
jobName String
813+
subscriberUrl String
814+
payload String
815+
startAfter DateTime
816+
retryCount Int @default(0)
817+
createdAt DateTime? @default(now())
818+
}
819+
810820
enum WorkflowTemplates {
811821
REMINDER
812822
CUSTOM

0 commit comments

Comments
 (0)