|
8 | 8 | Tracer, |
9 | 9 | } from "@internal/tracing"; |
10 | 10 | import { Logger } from "@trigger.dev/core/logger"; |
11 | | -import { PrismaClient } from "@trigger.dev/database"; |
| 11 | +import { PrismaClient, TaskSchedule, TaskScheduleInstance } from "@trigger.dev/database"; |
12 | 12 | import { Worker, type JobHandlerParams } from "@trigger.dev/redis-worker"; |
13 | 13 | import { calculateDistributedExecutionTime } from "./distributedScheduling.js"; |
14 | 14 | import { calculateNextScheduledTimestamp, nextScheduledTimestamps } from "./scheduleCalculation.js"; |
@@ -645,6 +645,140 @@ export class ScheduleEngine { |
645 | 645 | }); |
646 | 646 | } |
647 | 647 |
|
| 648 | + public recoverSchedulesInEnvironment(projectId: string, environmentId: string) { |
| 649 | + return startSpan(this.tracer, "recoverSchedulesInEnvironment", async (span) => { |
| 650 | + this.logger.info("Recovering schedules in environment", { |
| 651 | + environmentId, |
| 652 | + projectId, |
| 653 | + }); |
| 654 | + |
| 655 | + span.setAttribute("environmentId", environmentId); |
| 656 | + |
| 657 | + const schedules = await this.prisma.taskSchedule.findMany({ |
| 658 | + where: { |
| 659 | + projectId, |
| 660 | + instances: { |
| 661 | + some: { |
| 662 | + environmentId, |
| 663 | + }, |
| 664 | + }, |
| 665 | + }, |
| 666 | + select: { |
| 667 | + id: true, |
| 668 | + generatorExpression: true, |
| 669 | + instances: { |
| 670 | + select: { |
| 671 | + id: true, |
| 672 | + environmentId: true, |
| 673 | + lastScheduledTimestamp: true, |
| 674 | + nextScheduledTimestamp: true, |
| 675 | + }, |
| 676 | + }, |
| 677 | + }, |
| 678 | + }); |
| 679 | + |
| 680 | + const instancesWithSchedule = schedules |
| 681 | + .map((schedule) => ({ |
| 682 | + schedule, |
| 683 | + instance: schedule.instances.find((instance) => instance.environmentId === environmentId), |
| 684 | + })) |
| 685 | + .filter((instance) => instance.instance) as Array<{ |
| 686 | + schedule: Omit<(typeof schedules)[number], "instances">; |
| 687 | + instance: NonNullable<(typeof schedules)[number]["instances"][number]>; |
| 688 | + }>; |
| 689 | + |
| 690 | + if (instancesWithSchedule.length === 0) { |
| 691 | + this.logger.info("No instances found for environment", { |
| 692 | + environmentId, |
| 693 | + projectId, |
| 694 | + }); |
| 695 | + |
| 696 | + return { |
| 697 | + recovered: [], |
| 698 | + skipped: [], |
| 699 | + }; |
| 700 | + } |
| 701 | + |
| 702 | + const results = { |
| 703 | + recovered: [], |
| 704 | + skipped: [], |
| 705 | + } as { recovered: string[]; skipped: string[] }; |
| 706 | + |
| 707 | + for (const { instance, schedule } of instancesWithSchedule) { |
| 708 | + this.logger.info("Recovering schedule", { |
| 709 | + schedule, |
| 710 | + instance, |
| 711 | + }); |
| 712 | + |
| 713 | + const [recoverError, result] = await tryCatch( |
| 714 | + this.#recoverTaskScheduleInstance({ instance, schedule }) |
| 715 | + ); |
| 716 | + |
| 717 | + if (recoverError) { |
| 718 | + this.logger.error("Error recovering schedule", { |
| 719 | + error: recoverError instanceof Error ? recoverError.message : String(recoverError), |
| 720 | + }); |
| 721 | + |
| 722 | + span.setAttribute("recover_error", true); |
| 723 | + span.setAttribute( |
| 724 | + "recover_error_message", |
| 725 | + recoverError instanceof Error ? recoverError.message : String(recoverError) |
| 726 | + ); |
| 727 | + } else { |
| 728 | + span.setAttribute("recover_success", true); |
| 729 | + |
| 730 | + if (result === "recovered") { |
| 731 | + results.recovered.push(instance.id); |
| 732 | + } else { |
| 733 | + results.skipped.push(instance.id); |
| 734 | + } |
| 735 | + } |
| 736 | + } |
| 737 | + |
| 738 | + return results; |
| 739 | + }); |
| 740 | + } |
| 741 | + |
| 742 | + async #recoverTaskScheduleInstance({ |
| 743 | + instance, |
| 744 | + schedule, |
| 745 | + }: { |
| 746 | + instance: { |
| 747 | + id: string; |
| 748 | + environmentId: string; |
| 749 | + lastScheduledTimestamp: Date | null; |
| 750 | + nextScheduledTimestamp: Date | null; |
| 751 | + }; |
| 752 | + schedule: { id: string; generatorExpression: string }; |
| 753 | + }) { |
| 754 | + // inspect the schedule worker to see if there is a job for this instance |
| 755 | + const job = await this.worker.getJob(`scheduled-task-instance:${instance.id}`); |
| 756 | + |
| 757 | + if (job) { |
| 758 | + this.logger.info("Job already exists for instance", { |
| 759 | + instanceId: instance.id, |
| 760 | + job, |
| 761 | + schedule, |
| 762 | + }); |
| 763 | + |
| 764 | + return "skipped"; |
| 765 | + } |
| 766 | + |
| 767 | + this.logger.info("No job found for instance, registering next run", { |
| 768 | + instanceId: instance.id, |
| 769 | + schedule, |
| 770 | + }); |
| 771 | + |
| 772 | + // If the job does not exist, register the next run |
| 773 | + await this.registerNextTaskScheduleInstance({ instanceId: instance.id }); |
| 774 | + |
| 775 | + return "recovered"; |
| 776 | + } |
| 777 | + |
| 778 | + async getJob(id: string) { |
| 779 | + return this.worker.getJob(id); |
| 780 | + } |
| 781 | + |
648 | 782 | async quit() { |
649 | 783 | this.logger.info("Shutting down schedule engine"); |
650 | 784 |
|
|
0 commit comments