Skip to content

Commit

Permalink
Add locks for long-running potentially conflicting tasks
Browse files Browse the repository at this point in the history
Fixes #2
  • Loading branch information
iansan5653 authored Jan 10, 2024
1 parent 9c23b83 commit 4f6e4a5
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/jobs/clearCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export function clearCalendar(
teamCalendarId: TeamCalendarId,
calendar = TeamCalendarController.read(teamCalendarId),
) {
// no need to lock since we already lock fullSyncCalendar

Logger.log(`Fully clearing calendar ${teamCalendarId}`);

if (!calendar) throw new Error("Failed to clear calendar: not found");
Expand Down
7 changes: 7 additions & 0 deletions src/jobs/fullSyncCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export function fullSyncCalendar(
teamCalendarId: TeamCalendarId,
calendar = TeamCalendarController.read(teamCalendarId),
) {
// Avoid concurrent syncs. Unfortunately it's likely this will result in hitting the 6 minute execution limit, but
// there's not much we can do. Ideally we'd kill the ongoing sync but that's much more complicated.
const lock = LockService.getUserLock();
lock.waitLock(120_000); // long timeout because full syncs take forever, and we really don't want to skip a full sync as it will leave us in a bad state

if (!calendar) throw new Error("Failed to sync calendar: not found");

Logger.log(`Fully wiping and repopulating ${calendar.name} (${teamCalendarId})`);
Expand Down Expand Up @@ -57,4 +62,6 @@ export function fullSyncCalendar(
timestamp: new Date(),
},
});

lock.releaseLock();
}
18 changes: 16 additions & 2 deletions src/jobs/syncTeamMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@ import { syncCalendarForTeamMember } from "./syncCalendarForTeamMember";

/** Sync the team member for all calendars they are part of. */
export function syncTeamMember(teamMember: string) {
const calendars = TeamCalendarController.readAll().filter(([, calendar]) =>
Object.keys(calendar.teamMembers).includes(teamMember),
// If an event is created and then updated very quickly, it would trigger a second sync before the first one is
// completed. This could create conflicts or even duplicate an event, so we lock to prevent this.

// It might be cleaner to lock syncCalendarForTeamMember but then we'd need to move the sync state updates into there
const lock = LockService.getUserLock();
lock.waitLock(10_000); // Short timeout - syncs should be fast, and skipping a sync is ok. Will throw on failure.

const calendars = TeamCalendarController.readAll().filter(
([, calendar]) =>
Object.keys(calendar.teamMembers).includes(teamMember) &&
// Locking only prevents concurrent syncs - full rebuilds aren't syncs. And rebuilds take a long time so conflicts
// are likely. If the calendar is rebuilding though it's fine to just skip the sync since everything is getting recreated.
calendar.syncStatus.state !== "building" &&
calendar.syncStatus.state !== "rebuilding",
);

Logger.log(`Syncing ${teamMember} for ${calendars.length} calendars`);
Expand All @@ -13,4 +25,6 @@ export function syncTeamMember(teamMember: string) {
syncCalendarForTeamMember(calendar, teamMember);
TeamCalendarController.update(id, { syncStatus: { state: "success", timestamp: new Date() } });
}

lock.releaseLock();
}
6 changes: 6 additions & 0 deletions src/jobs/updateSyncTriggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { TeamCalendarController } from "../controllers/TeamCalendarController";

/** Update all sync triggers for all calendars. */
export function updateSyncTriggers() {
// This can take a while, so let's avoid concurrent runs to be safe
const lock = LockService.getUserLock();
lock.waitLock(15_000);

Logger.log("Updating all sync triggers");

const allTeamMembers = new Set(
Expand All @@ -18,4 +22,6 @@ export function updateSyncTriggers() {
SyncTriggerController.delete(existingTriggerTeamMember);

for (const teamMember of allTeamMembers) SyncTriggerController.create(teamMember);

lock.releaseLock();
}

0 comments on commit 4f6e4a5

Please sign in to comment.