@sentry/junior-scheduler 0.75.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +153 -135
- package/dist/plugin.d.ts +1 -1
- package/dist/schedule-tools.d.ts +2 -2
- package/dist/store.d.ts +8 -4
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/plugin.ts +11 -28
- package/src/schedule-tools.ts +11 -7
- package/src/store.ts +157 -137
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createSchedulerPlugin, schedulerPlugin } from "./plugin";
|
|
2
2
|
export { createSlackScheduleCreateTaskTool, createSlackScheduleDeleteTaskTool, createSlackScheduleListTasksTool, createSlackScheduleRunTaskNowTool, createSlackScheduleUpdateTaskTool, type SchedulerToolContext, } from "./schedule-tools";
|
|
3
|
-
export { createSchedulerOperationalSqlStore, createSchedulerSqlStore, migrateSchedulerStateToSql, } from "./store";
|
|
3
|
+
export { createSchedulerOperationalSqlStore, createSchedulerSqlStore, migrateSchedulerStateToSql, type SchedulerDb, } from "./store";
|
|
4
4
|
export type { ScheduledCalendarFrequency, ScheduledLocalTime, ScheduledRun, ScheduledRunStatus, ScheduledTask, ScheduledTaskConversationAccess, ScheduledTaskExecutionActor, ScheduledTaskPrincipal, ScheduledTaskRecurrence, ScheduledTaskSchedule, ScheduledTaskSpec, ScheduledTaskStatus, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
2
|
import {
|
|
3
|
+
createSlackSource,
|
|
3
4
|
defineJuniorPlugin
|
|
4
5
|
} from "@sentry/junior-plugin-api";
|
|
5
6
|
|
|
@@ -9,6 +10,17 @@ import {
|
|
|
9
10
|
destinationSchema,
|
|
10
11
|
isSlackDestination
|
|
11
12
|
} from "@sentry/junior-plugin-api";
|
|
13
|
+
import {
|
|
14
|
+
and,
|
|
15
|
+
asc,
|
|
16
|
+
eq,
|
|
17
|
+
inArray,
|
|
18
|
+
isNotNull,
|
|
19
|
+
lte,
|
|
20
|
+
ne,
|
|
21
|
+
or,
|
|
22
|
+
sql as sql2
|
|
23
|
+
} from "drizzle-orm";
|
|
12
24
|
import { z } from "zod";
|
|
13
25
|
|
|
14
26
|
// src/cadence.ts
|
|
@@ -97,7 +109,7 @@ function localDateTimeToTimestampMs(args) {
|
|
|
97
109
|
0
|
|
98
110
|
);
|
|
99
111
|
let timestampMs = localAsUtcMs - getTimeZoneOffsetMs(localAsUtcMs, args.timezone);
|
|
100
|
-
for (let
|
|
112
|
+
for (let index2 = 0; index2 < 3; index2 += 1) {
|
|
101
113
|
const next = localAsUtcMs - getTimeZoneOffsetMs(timestampMs, args.timezone);
|
|
102
114
|
if (next === timestampMs) {
|
|
103
115
|
break;
|
|
@@ -358,6 +370,52 @@ function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
|
|
|
358
370
|
});
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
// src/db/schema.ts
|
|
374
|
+
import { sql } from "drizzle-orm";
|
|
375
|
+
import { bigint, index, jsonb, pgTable, text } from "drizzle-orm/pg-core";
|
|
376
|
+
var juniorSchedulerTasks = pgTable(
|
|
377
|
+
"junior_scheduler_tasks",
|
|
378
|
+
{
|
|
379
|
+
id: text("id").primaryKey(),
|
|
380
|
+
teamId: text("team_id").notNull(),
|
|
381
|
+
status: text("status").notNull(),
|
|
382
|
+
nextRunAtMs: bigint("next_run_at_ms", { mode: "number" }),
|
|
383
|
+
runNowAtMs: bigint("run_now_at_ms", { mode: "number" }),
|
|
384
|
+
createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
385
|
+
record: jsonb("record").$type().notNull()
|
|
386
|
+
},
|
|
387
|
+
(table) => [
|
|
388
|
+
index("junior_scheduler_tasks_team_status_idx").on(table.teamId, table.createdAtMs, table.id).where(sql`${table.status} <> 'deleted'`),
|
|
389
|
+
index("junior_scheduler_tasks_run_now_due_idx").on(table.runNowAtMs, table.createdAtMs, table.id).where(
|
|
390
|
+
sql`${table.status} = 'active' AND ${table.runNowAtMs} IS NOT NULL`
|
|
391
|
+
),
|
|
392
|
+
index("junior_scheduler_tasks_next_run_due_idx").on(table.nextRunAtMs, table.createdAtMs, table.id).where(
|
|
393
|
+
sql`${table.status} = 'active' AND ${table.nextRunAtMs} IS NOT NULL`
|
|
394
|
+
)
|
|
395
|
+
]
|
|
396
|
+
);
|
|
397
|
+
var juniorSchedulerRuns = pgTable(
|
|
398
|
+
"junior_scheduler_runs",
|
|
399
|
+
{
|
|
400
|
+
id: text("id").primaryKey(),
|
|
401
|
+
taskId: text("task_id").notNull(),
|
|
402
|
+
status: text("status").notNull(),
|
|
403
|
+
scheduledForMs: bigint("scheduled_for_ms", { mode: "number" }).notNull(),
|
|
404
|
+
record: jsonb("record").$type().notNull()
|
|
405
|
+
},
|
|
406
|
+
(table) => [
|
|
407
|
+
index("junior_scheduler_runs_task_status_idx").on(
|
|
408
|
+
table.taskId,
|
|
409
|
+
table.status,
|
|
410
|
+
table.scheduledForMs
|
|
411
|
+
),
|
|
412
|
+
index("junior_scheduler_runs_status_idx").on(
|
|
413
|
+
table.status,
|
|
414
|
+
table.scheduledForMs
|
|
415
|
+
)
|
|
416
|
+
]
|
|
417
|
+
);
|
|
418
|
+
|
|
361
419
|
// src/store.ts
|
|
362
420
|
var SCHEDULER_KEY_PREFIX = "junior:scheduler";
|
|
363
421
|
var SCHEDULER_RECORD_TTL_MS = 5 * 365 * 24 * 60 * 60 * 1e3;
|
|
@@ -616,111 +674,74 @@ function parseSqlRunRow(row) {
|
|
|
616
674
|
const parsed = runRecordSchema.safeParse(parseJsonRecord(row.record));
|
|
617
675
|
return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
|
|
618
676
|
}
|
|
619
|
-
function json(value) {
|
|
620
|
-
return JSON.stringify(value);
|
|
621
|
-
}
|
|
622
677
|
async function withSqlLock(db, key, callback) {
|
|
623
678
|
return await db.transaction(async (tx) => {
|
|
624
|
-
await tx.execute(
|
|
679
|
+
await tx.execute(sql2`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
|
|
625
680
|
return await callback(tx);
|
|
626
681
|
});
|
|
627
682
|
}
|
|
628
683
|
async function upsertSqlTask(db, task) {
|
|
629
|
-
await db.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
record = EXCLUDED.record
|
|
649
|
-
`,
|
|
650
|
-
[
|
|
651
|
-
task.id,
|
|
652
|
-
task.destination.teamId,
|
|
653
|
-
task.status,
|
|
654
|
-
task.nextRunAtMs ?? null,
|
|
655
|
-
task.runNowAtMs ?? null,
|
|
656
|
-
task.createdAtMs,
|
|
657
|
-
json(task)
|
|
658
|
-
]
|
|
659
|
-
);
|
|
684
|
+
await db.insert(juniorSchedulerTasks).values({
|
|
685
|
+
createdAtMs: task.createdAtMs,
|
|
686
|
+
id: task.id,
|
|
687
|
+
nextRunAtMs: task.nextRunAtMs,
|
|
688
|
+
record: task,
|
|
689
|
+
runNowAtMs: task.runNowAtMs,
|
|
690
|
+
status: task.status,
|
|
691
|
+
teamId: task.destination.teamId
|
|
692
|
+
}).onConflictDoUpdate({
|
|
693
|
+
target: juniorSchedulerTasks.id,
|
|
694
|
+
set: {
|
|
695
|
+
createdAtMs: sql2`excluded.created_at_ms`,
|
|
696
|
+
nextRunAtMs: sql2`excluded.next_run_at_ms`,
|
|
697
|
+
record: sql2`excluded.record`,
|
|
698
|
+
runNowAtMs: sql2`excluded.run_now_at_ms`,
|
|
699
|
+
status: sql2`excluded.status`,
|
|
700
|
+
teamId: sql2`excluded.team_id`
|
|
701
|
+
}
|
|
702
|
+
});
|
|
660
703
|
}
|
|
661
704
|
async function upsertSqlRun(db, run) {
|
|
662
|
-
await db.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
record = EXCLUDED.record
|
|
678
|
-
`,
|
|
679
|
-
[run.id, run.taskId, run.status, run.scheduledForMs, json(run)]
|
|
680
|
-
);
|
|
705
|
+
await db.insert(juniorSchedulerRuns).values({
|
|
706
|
+
id: run.id,
|
|
707
|
+
record: run,
|
|
708
|
+
scheduledForMs: run.scheduledForMs,
|
|
709
|
+
status: run.status,
|
|
710
|
+
taskId: run.taskId
|
|
711
|
+
}).onConflictDoUpdate({
|
|
712
|
+
target: juniorSchedulerRuns.id,
|
|
713
|
+
set: {
|
|
714
|
+
record: sql2`excluded.record`,
|
|
715
|
+
scheduledForMs: sql2`excluded.scheduled_for_ms`,
|
|
716
|
+
status: sql2`excluded.status`,
|
|
717
|
+
taskId: sql2`excluded.task_id`
|
|
718
|
+
}
|
|
719
|
+
});
|
|
681
720
|
}
|
|
682
721
|
async function getTaskFromSql(db, taskId) {
|
|
683
|
-
const rows = await db.
|
|
684
|
-
"SELECT record FROM junior_scheduler_tasks WHERE id = $1",
|
|
685
|
-
[taskId]
|
|
686
|
-
);
|
|
722
|
+
const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
|
|
687
723
|
return rows[0] ? parseSqlTaskRow(rows[0]) : void 0;
|
|
688
724
|
}
|
|
689
725
|
async function getRunFromSql(db, runId) {
|
|
690
|
-
const rows = await db.
|
|
691
|
-
"SELECT record FROM junior_scheduler_runs WHERE id = $1",
|
|
692
|
-
[runId]
|
|
693
|
-
);
|
|
694
|
-
return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
|
|
695
|
-
}
|
|
696
|
-
async function getClaimedRunSlotFromSql(db, runId) {
|
|
697
|
-
const rows = await db.query(
|
|
698
|
-
"SELECT record FROM junior_scheduler_runs WHERE id = $1",
|
|
699
|
-
[runId]
|
|
700
|
-
);
|
|
726
|
+
const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(eq(juniorSchedulerRuns.id, runId)).limit(1);
|
|
701
727
|
return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
|
|
702
728
|
}
|
|
703
729
|
async function listTasksFromSql(db) {
|
|
704
|
-
const rows = await db.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
FROM junior_scheduler_tasks
|
|
708
|
-
WHERE status <> 'deleted'
|
|
709
|
-
ORDER BY created_at_ms ASC, id ASC
|
|
710
|
-
`
|
|
730
|
+
const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(ne(juniorSchedulerTasks.status, "deleted")).orderBy(
|
|
731
|
+
asc(juniorSchedulerTasks.createdAtMs),
|
|
732
|
+
asc(juniorSchedulerTasks.id)
|
|
711
733
|
);
|
|
712
734
|
return rows.map(parseSqlTaskRow).filter(present);
|
|
713
735
|
}
|
|
714
736
|
async function listTasksForTeamFromSql(db, teamId) {
|
|
715
|
-
const rows = await db.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
[teamId]
|
|
737
|
+
const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(
|
|
738
|
+
and(
|
|
739
|
+
eq(juniorSchedulerTasks.teamId, teamId),
|
|
740
|
+
ne(juniorSchedulerTasks.status, "deleted")
|
|
741
|
+
)
|
|
742
|
+
).orderBy(
|
|
743
|
+
asc(juniorSchedulerTasks.createdAtMs),
|
|
744
|
+
asc(juniorSchedulerTasks.id)
|
|
724
745
|
);
|
|
725
746
|
return rows.map(parseSqlTaskRow).filter(present);
|
|
726
747
|
}
|
|
@@ -728,15 +749,17 @@ async function listIncompleteRunsForTasksFromSql(db, tasks) {
|
|
|
728
749
|
if (tasks.length === 0) {
|
|
729
750
|
return [];
|
|
730
751
|
}
|
|
731
|
-
const rows = await db.
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
752
|
+
const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(
|
|
753
|
+
and(
|
|
754
|
+
inArray(
|
|
755
|
+
juniorSchedulerRuns.taskId,
|
|
756
|
+
tasks.map((task) => task.id)
|
|
757
|
+
),
|
|
758
|
+
inArray(juniorSchedulerRuns.status, [...SQL_INCOMPLETE_RUN_STATUSES])
|
|
759
|
+
)
|
|
760
|
+
).orderBy(
|
|
761
|
+
asc(juniorSchedulerRuns.scheduledForMs),
|
|
762
|
+
asc(juniorSchedulerRuns.id)
|
|
740
763
|
);
|
|
741
764
|
return rows.map(parseSqlRunRow).filter(present);
|
|
742
765
|
}
|
|
@@ -754,9 +777,11 @@ var SqlSchedulerStore = class {
|
|
|
754
777
|
}
|
|
755
778
|
async saveTaskRecord(db, task, current) {
|
|
756
779
|
if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) {
|
|
757
|
-
await db.
|
|
758
|
-
|
|
759
|
-
|
|
780
|
+
await db.delete(juniorSchedulerRuns).where(
|
|
781
|
+
and(
|
|
782
|
+
eq(juniorSchedulerRuns.id, buildRunId(task.id, task.nextRunAtMs)),
|
|
783
|
+
eq(juniorSchedulerRuns.status, "blocked")
|
|
784
|
+
)
|
|
760
785
|
);
|
|
761
786
|
}
|
|
762
787
|
await upsertSqlTask(db, task);
|
|
@@ -772,18 +797,23 @@ var SqlSchedulerStore = class {
|
|
|
772
797
|
}
|
|
773
798
|
async claimDueRun(args) {
|
|
774
799
|
return await withSqlLock(this.db, "junior:scheduler:claim", async (db) => {
|
|
775
|
-
const rows = await db.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
800
|
+
const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(
|
|
801
|
+
and(
|
|
802
|
+
eq(juniorSchedulerTasks.status, "active"),
|
|
803
|
+
or(
|
|
804
|
+
and(
|
|
805
|
+
isNotNull(juniorSchedulerTasks.runNowAtMs),
|
|
806
|
+
lte(juniorSchedulerTasks.runNowAtMs, args.nowMs)
|
|
807
|
+
),
|
|
808
|
+
and(
|
|
809
|
+
isNotNull(juniorSchedulerTasks.nextRunAtMs),
|
|
810
|
+
lte(juniorSchedulerTasks.nextRunAtMs, args.nowMs)
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
).orderBy(
|
|
815
|
+
asc(juniorSchedulerTasks.createdAtMs),
|
|
816
|
+
asc(juniorSchedulerTasks.id)
|
|
787
817
|
);
|
|
788
818
|
for (const row of rows) {
|
|
789
819
|
const task = parseSqlTaskRow(row);
|
|
@@ -817,7 +847,7 @@ ORDER BY created_at_ms ASC, id ASC
|
|
|
817
847
|
await upsertSqlRun(db, reclaimed);
|
|
818
848
|
return reclaimed;
|
|
819
849
|
}
|
|
820
|
-
const existingRun = await
|
|
850
|
+
const existingRun = await getRunFromSql(db, runId);
|
|
821
851
|
if (existingRun) {
|
|
822
852
|
continue;
|
|
823
853
|
}
|
|
@@ -1124,8 +1154,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
1124
1154
|
import {
|
|
1125
1155
|
PluginToolInputError,
|
|
1126
1156
|
pluginCredentialSubjectSchema as pluginCredentialSubjectSchema2,
|
|
1127
|
-
|
|
1128
|
-
isSlackDestination as isSlackDestination2
|
|
1157
|
+
sourceSchema
|
|
1129
1158
|
} from "@sentry/junior-plugin-api";
|
|
1130
1159
|
|
|
1131
1160
|
// src/types.ts
|
|
@@ -1142,7 +1171,7 @@ function throwToolInputError(error) {
|
|
|
1142
1171
|
throw new PluginToolInputError(error);
|
|
1143
1172
|
}
|
|
1144
1173
|
function requireActiveConversation(context) {
|
|
1145
|
-
const parsed =
|
|
1174
|
+
const parsed = sourceSchema.safeParse(context.source);
|
|
1146
1175
|
if (!parsed.success) {
|
|
1147
1176
|
const source = context.source;
|
|
1148
1177
|
const issues = parsed.error.issues;
|
|
@@ -1162,10 +1191,14 @@ function requireActiveConversation(context) {
|
|
|
1162
1191
|
}
|
|
1163
1192
|
throwToolInputError("No active Slack conversation is available.");
|
|
1164
1193
|
}
|
|
1165
|
-
if (
|
|
1194
|
+
if (parsed.data.platform !== "slack") {
|
|
1166
1195
|
throwToolInputError("No active Slack conversation is available.");
|
|
1167
1196
|
}
|
|
1168
|
-
return
|
|
1197
|
+
return {
|
|
1198
|
+
platform: "slack",
|
|
1199
|
+
teamId: parsed.data.teamId,
|
|
1200
|
+
channelId: parsed.data.channelId
|
|
1201
|
+
};
|
|
1169
1202
|
}
|
|
1170
1203
|
function requireRequester(context) {
|
|
1171
1204
|
if (context.requester?.platform !== "slack") {
|
|
@@ -1653,22 +1686,15 @@ function buildScheduledTaskDispatchMetadata(args) {
|
|
|
1653
1686
|
};
|
|
1654
1687
|
}
|
|
1655
1688
|
function scheduledTaskDispatchSource(task) {
|
|
1656
|
-
return {
|
|
1657
|
-
platform: "slack",
|
|
1689
|
+
return createSlackSource({
|
|
1658
1690
|
teamId: task.destination.teamId,
|
|
1659
1691
|
channelId: task.destination.channelId
|
|
1660
|
-
};
|
|
1692
|
+
});
|
|
1661
1693
|
}
|
|
1662
1694
|
function schedulerStore2(ctx) {
|
|
1663
|
-
if (!ctx.db) {
|
|
1664
|
-
throw new Error("Scheduler plugin requires ctx.db");
|
|
1665
|
-
}
|
|
1666
1695
|
return createSchedulerSqlStore(ctx.db);
|
|
1667
1696
|
}
|
|
1668
1697
|
function schedulerOperationalStore(ctx) {
|
|
1669
|
-
if (!ctx.db) {
|
|
1670
|
-
throw new Error("Scheduler plugin requires ctx.db");
|
|
1671
|
-
}
|
|
1672
1698
|
return createSchedulerOperationalSqlStore(ctx.db);
|
|
1673
1699
|
}
|
|
1674
1700
|
function shouldSkipRun(task, run) {
|
|
@@ -1686,11 +1712,7 @@ function shouldSkipRun(task, run) {
|
|
|
1686
1712
|
function createSchedulerToolContext(ctx) {
|
|
1687
1713
|
return {
|
|
1688
1714
|
credentialSubject: ctx.slack?.credentialSubject,
|
|
1689
|
-
source: ctx.source.platform === "slack" ?
|
|
1690
|
-
platform: "slack",
|
|
1691
|
-
teamId: ctx.source.teamId,
|
|
1692
|
-
channelId: ctx.source.channelId
|
|
1693
|
-
} : void 0,
|
|
1715
|
+
source: ctx.source.platform === "slack" ? ctx.source : void 0,
|
|
1694
1716
|
requester: ctx.requester?.platform === "slack" ? ctx.requester : void 0,
|
|
1695
1717
|
store: schedulerStore2(ctx),
|
|
1696
1718
|
userText: ctx.userText
|
|
@@ -1937,7 +1959,6 @@ async function buildSchedulerOperationalReport(args) {
|
|
|
1937
1959
|
}
|
|
1938
1960
|
function createSchedulerPlugin() {
|
|
1939
1961
|
return defineJuniorPlugin({
|
|
1940
|
-
database: {},
|
|
1941
1962
|
manifest: {
|
|
1942
1963
|
name: "scheduler",
|
|
1943
1964
|
displayName: "Scheduler",
|
|
@@ -1985,7 +2006,7 @@ function createSchedulerPlugin() {
|
|
|
1985
2006
|
processedCount += 1;
|
|
1986
2007
|
}
|
|
1987
2008
|
}
|
|
1988
|
-
for (let
|
|
2009
|
+
for (let index2 = processedCount; index2 < SCHEDULER_HEARTBEAT_LIMIT; index2 += 1) {
|
|
1989
2010
|
const run = await store.claimDueRun({ nowMs: ctx.nowMs });
|
|
1990
2011
|
if (!run) {
|
|
1991
2012
|
break;
|
|
@@ -2062,9 +2083,6 @@ function createSchedulerPlugin() {
|
|
|
2062
2083
|
});
|
|
2063
2084
|
},
|
|
2064
2085
|
async migrateStorage(ctx) {
|
|
2065
|
-
if (!ctx.db) {
|
|
2066
|
-
throw new Error("Scheduler storage migration requires ctx.db");
|
|
2067
|
-
}
|
|
2068
2086
|
return await migrateSchedulerStateToSql({
|
|
2069
2087
|
db: ctx.db,
|
|
2070
2088
|
state: ctx.state
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Create Junior's built-in trusted scheduler plugin. */
|
|
2
2
|
export declare function createSchedulerPlugin(): import("@sentry/junior-plugin-api").PluginRegistration;
|
|
3
|
-
/** Register
|
|
3
|
+
/** Register scheduler runtime hooks for scheduled Junior tasks. */
|
|
4
4
|
export declare const schedulerPlugin: typeof createSchedulerPlugin;
|
package/dist/schedule-tools.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type PluginCredentialSubject, type PluginToolDefinition, type
|
|
1
|
+
import { type PluginCredentialSubject, type PluginToolDefinition, type SlackRequester, type SlackSource } from "@sentry/junior-plugin-api";
|
|
2
2
|
import { type SchedulerStore } from "./store";
|
|
3
3
|
export interface SchedulerToolContext {
|
|
4
4
|
credentialSubject?: PluginCredentialSubject;
|
|
5
5
|
requester?: SlackRequester;
|
|
6
|
-
source?:
|
|
6
|
+
source?: SlackSource;
|
|
7
7
|
store: SchedulerStore;
|
|
8
8
|
userText?: string;
|
|
9
9
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type PluginReadState, type PluginState } from "@sentry/junior-plugin-api";
|
|
2
|
+
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
3
|
+
import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
|
|
4
|
+
import * as schedulerSqlSchema from "./db/schema";
|
|
2
5
|
import type { ScheduledRun, ScheduledTask } from "./types";
|
|
6
|
+
export type SchedulerDb = PgDatabase<PgQueryResultHKT, typeof schedulerSqlSchema>;
|
|
3
7
|
export interface SchedulerStore {
|
|
4
8
|
claimDueRun(args: {
|
|
5
9
|
nowMs: number;
|
|
@@ -55,12 +59,12 @@ export declare function createSchedulerStore(state: PluginState): SchedulerStore
|
|
|
55
59
|
/** Create a read-only scheduler store for operational reporting. */
|
|
56
60
|
export declare function createSchedulerOperationalStore(state: PluginReadState): SchedulerOperationalStore;
|
|
57
61
|
/** Create a scheduler store backed by the plugin SQL database. */
|
|
58
|
-
export declare function createSchedulerSqlStore(db:
|
|
62
|
+
export declare function createSchedulerSqlStore(db: SchedulerDb): SchedulerStore;
|
|
59
63
|
/** Create a read-only scheduler operational store backed by SQL. */
|
|
60
|
-
export declare function createSchedulerOperationalSqlStore(db:
|
|
64
|
+
export declare function createSchedulerOperationalSqlStore(db: SchedulerDb): SchedulerOperationalStore;
|
|
61
65
|
/** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
|
|
62
66
|
export declare function migrateSchedulerStateToSql(args: {
|
|
63
|
-
db:
|
|
67
|
+
db: SchedulerDb;
|
|
64
68
|
state: PluginState;
|
|
65
69
|
}): Promise<{
|
|
66
70
|
existing: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-scheduler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@sinclair/typebox": "^0.34.49",
|
|
27
27
|
"drizzle-orm": "^0.45.2",
|
|
28
28
|
"zod": "^4.4.3",
|
|
29
|
-
"@sentry/junior-plugin-api": "0.
|
|
29
|
+
"@sentry/junior-plugin-api": "0.76.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^25.9.1",
|
package/src/index.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createSlackSource,
|
|
2
3
|
defineJuniorPlugin,
|
|
3
4
|
type Dispatch,
|
|
4
|
-
type PluginDb,
|
|
5
5
|
type PluginToolDefinition,
|
|
6
6
|
type PluginOperationalReportContent,
|
|
7
7
|
type PluginReadState,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createSchedulerOperationalSqlStore,
|
|
15
15
|
createSchedulerSqlStore,
|
|
16
16
|
migrateSchedulerStateToSql,
|
|
17
|
+
type SchedulerDb,
|
|
17
18
|
type SchedulerOperationalStore,
|
|
18
19
|
type SchedulerStore,
|
|
19
20
|
} from "./store";
|
|
@@ -76,27 +77,20 @@ function buildScheduledTaskDispatchMetadata(args: {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
function scheduledTaskDispatchSource(task: ScheduledTask): Source {
|
|
79
|
-
return {
|
|
80
|
-
platform: "slack",
|
|
80
|
+
return createSlackSource({
|
|
81
81
|
teamId: task.destination.teamId,
|
|
82
82
|
channelId: task.destination.channelId,
|
|
83
|
-
};
|
|
83
|
+
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function schedulerStore(ctx: { db
|
|
87
|
-
|
|
88
|
-
throw new Error("Scheduler plugin requires ctx.db");
|
|
89
|
-
}
|
|
90
|
-
return createSchedulerSqlStore(ctx.db);
|
|
86
|
+
function schedulerStore(ctx: { db: unknown }): SchedulerStore {
|
|
87
|
+
return createSchedulerSqlStore(ctx.db as SchedulerDb);
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
function schedulerOperationalStore(ctx: {
|
|
94
|
-
db
|
|
91
|
+
db: unknown;
|
|
95
92
|
}): SchedulerOperationalStore {
|
|
96
|
-
|
|
97
|
-
throw new Error("Scheduler plugin requires ctx.db");
|
|
98
|
-
}
|
|
99
|
-
return createSchedulerOperationalSqlStore(ctx.db);
|
|
93
|
+
return createSchedulerOperationalSqlStore(ctx.db as SchedulerDb);
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
function shouldSkipRun(
|
|
@@ -123,14 +117,7 @@ function createSchedulerToolContext(
|
|
|
123
117
|
): SchedulerToolContext {
|
|
124
118
|
return {
|
|
125
119
|
credentialSubject: ctx.slack?.credentialSubject,
|
|
126
|
-
source:
|
|
127
|
-
ctx.source.platform === "slack"
|
|
128
|
-
? {
|
|
129
|
-
platform: "slack",
|
|
130
|
-
teamId: ctx.source.teamId,
|
|
131
|
-
channelId: ctx.source.channelId,
|
|
132
|
-
}
|
|
133
|
-
: undefined,
|
|
120
|
+
source: ctx.source.platform === "slack" ? ctx.source : undefined,
|
|
134
121
|
requester: ctx.requester?.platform === "slack" ? ctx.requester : undefined,
|
|
135
122
|
store: schedulerStore(ctx),
|
|
136
123
|
userText: ctx.userText,
|
|
@@ -430,7 +417,6 @@ async function buildSchedulerOperationalReport(args: {
|
|
|
430
417
|
/** Create Junior's built-in trusted scheduler plugin. */
|
|
431
418
|
export function createSchedulerPlugin() {
|
|
432
419
|
return defineJuniorPlugin({
|
|
433
|
-
database: {},
|
|
434
420
|
manifest: {
|
|
435
421
|
name: "scheduler",
|
|
436
422
|
displayName: "Scheduler",
|
|
@@ -575,11 +561,8 @@ export function createSchedulerPlugin() {
|
|
|
575
561
|
});
|
|
576
562
|
},
|
|
577
563
|
async migrateStorage(ctx) {
|
|
578
|
-
if (!ctx.db) {
|
|
579
|
-
throw new Error("Scheduler storage migration requires ctx.db");
|
|
580
|
-
}
|
|
581
564
|
return await migrateSchedulerStateToSql({
|
|
582
|
-
db: ctx.db,
|
|
565
|
+
db: ctx.db as SchedulerDb,
|
|
583
566
|
state: ctx.state,
|
|
584
567
|
});
|
|
585
568
|
},
|
|
@@ -587,5 +570,5 @@ export function createSchedulerPlugin() {
|
|
|
587
570
|
});
|
|
588
571
|
}
|
|
589
572
|
|
|
590
|
-
/** Register
|
|
573
|
+
/** Register scheduler runtime hooks for scheduled Junior tasks. */
|
|
591
574
|
export const schedulerPlugin = createSchedulerPlugin;
|
package/src/schedule-tools.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { Type } from "@sinclair/typebox";
|
|
|
3
3
|
import {
|
|
4
4
|
PluginToolInputError,
|
|
5
5
|
pluginCredentialSubjectSchema,
|
|
6
|
-
|
|
7
|
-
isSlackDestination,
|
|
6
|
+
sourceSchema,
|
|
8
7
|
type PluginCredentialSubject,
|
|
9
8
|
type PluginToolDefinition,
|
|
10
9
|
type SlackDestination,
|
|
11
10
|
type SlackRequester,
|
|
11
|
+
type SlackSource,
|
|
12
12
|
} from "@sentry/junior-plugin-api";
|
|
13
13
|
import { buildCalendarRecurrence, parseScheduleTimestamp } from "./cadence";
|
|
14
14
|
import { sanitizeScheduledTaskPrincipal } from "./identity";
|
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
export interface SchedulerToolContext {
|
|
27
27
|
credentialSubject?: PluginCredentialSubject;
|
|
28
28
|
requester?: SlackRequester;
|
|
29
|
-
source?:
|
|
29
|
+
source?: SlackSource;
|
|
30
30
|
store: SchedulerStore;
|
|
31
31
|
userText?: string;
|
|
32
32
|
}
|
|
@@ -47,9 +47,9 @@ function throwToolInputError(error: string): never {
|
|
|
47
47
|
function requireActiveConversation(
|
|
48
48
|
context: SchedulerToolContext,
|
|
49
49
|
): SlackDestination {
|
|
50
|
-
const parsed =
|
|
50
|
+
const parsed = sourceSchema.safeParse(context.source);
|
|
51
51
|
if (!parsed.success) {
|
|
52
|
-
const source = context.source as Partial<
|
|
52
|
+
const source = context.source as Partial<SlackSource> | undefined;
|
|
53
53
|
const issues = parsed.error.issues as readonly SchemaIssue[];
|
|
54
54
|
if (!source || source.platform !== "slack") {
|
|
55
55
|
throwToolInputError("No active Slack conversation is available.");
|
|
@@ -68,11 +68,15 @@ function requireActiveConversation(
|
|
|
68
68
|
throwToolInputError("No active Slack conversation is available.");
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
if (
|
|
71
|
+
if (parsed.data.platform !== "slack") {
|
|
72
72
|
throwToolInputError("No active Slack conversation is available.");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
platform: "slack",
|
|
77
|
+
teamId: parsed.data.teamId,
|
|
78
|
+
channelId: parsed.data.channelId,
|
|
79
|
+
};
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
function requireRequester(
|
package/src/store.ts
CHANGED
|
@@ -2,12 +2,26 @@ import {
|
|
|
2
2
|
pluginCredentialSubjectSchema,
|
|
3
3
|
destinationSchema,
|
|
4
4
|
isSlackDestination,
|
|
5
|
-
type PluginDb,
|
|
6
5
|
type PluginReadState,
|
|
7
6
|
type PluginState,
|
|
8
7
|
} from "@sentry/junior-plugin-api";
|
|
8
|
+
import {
|
|
9
|
+
and,
|
|
10
|
+
asc,
|
|
11
|
+
eq,
|
|
12
|
+
inArray,
|
|
13
|
+
isNotNull,
|
|
14
|
+
lte,
|
|
15
|
+
ne,
|
|
16
|
+
or,
|
|
17
|
+
sql,
|
|
18
|
+
} from "drizzle-orm";
|
|
19
|
+
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
20
|
+
import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
|
|
9
21
|
import { z } from "zod";
|
|
10
22
|
import { getNextRunAtMs } from "./cadence";
|
|
23
|
+
import * as schedulerSqlSchema from "./db/schema";
|
|
24
|
+
import { juniorSchedulerRuns, juniorSchedulerTasks } from "./db/schema";
|
|
11
25
|
import type { ScheduledRun, ScheduledTask } from "./types";
|
|
12
26
|
|
|
13
27
|
const SCHEDULER_KEY_PREFIX = "junior:scheduler";
|
|
@@ -18,6 +32,10 @@ const PENDING_CLAIM_STALE_MS = 60_000;
|
|
|
18
32
|
const MISSED_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
19
33
|
const LOCK_TTL_MS = 10_000;
|
|
20
34
|
const SQL_INCOMPLETE_RUN_STATUSES = ["pending", "running"] as const;
|
|
35
|
+
export type SchedulerDb = PgDatabase<
|
|
36
|
+
PgQueryResultHKT,
|
|
37
|
+
typeof schedulerSqlSchema
|
|
38
|
+
>;
|
|
21
39
|
const slackDestinationSchema = destinationSchema.refine(isSlackDestination);
|
|
22
40
|
const taskPrincipalSchema = z
|
|
23
41
|
.object({
|
|
@@ -1055,161 +1073,150 @@ function parseSqlRunRow(row: SchedulerRunRow): ScheduledRun | undefined {
|
|
|
1055
1073
|
return parsed.success ? stripLegacyRunFields(parsed.data) : undefined;
|
|
1056
1074
|
}
|
|
1057
1075
|
|
|
1058
|
-
function json(value: unknown): string {
|
|
1059
|
-
return JSON.stringify(value);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
1076
|
async function withSqlLock<T>(
|
|
1063
|
-
db:
|
|
1077
|
+
db: SchedulerDb,
|
|
1064
1078
|
key: string,
|
|
1065
|
-
callback: (db:
|
|
1079
|
+
callback: (db: SchedulerDb) => Promise<T>,
|
|
1066
1080
|
): Promise<T> {
|
|
1067
1081
|
return await db.transaction(async (tx) => {
|
|
1068
|
-
await tx.execute(
|
|
1082
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
|
|
1069
1083
|
return await callback(tx);
|
|
1070
1084
|
});
|
|
1071
1085
|
}
|
|
1072
1086
|
|
|
1073
|
-
async function upsertSqlTask(
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
`,
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
task.nextRunAtMs ?? null,
|
|
1100
|
-
task.runNowAtMs ?? null,
|
|
1101
|
-
task.createdAtMs,
|
|
1102
|
-
json(task),
|
|
1103
|
-
],
|
|
1104
|
-
);
|
|
1087
|
+
async function upsertSqlTask(
|
|
1088
|
+
db: SchedulerDb,
|
|
1089
|
+
task: ScheduledTask,
|
|
1090
|
+
): Promise<void> {
|
|
1091
|
+
await db
|
|
1092
|
+
.insert(juniorSchedulerTasks)
|
|
1093
|
+
.values({
|
|
1094
|
+
createdAtMs: task.createdAtMs,
|
|
1095
|
+
id: task.id,
|
|
1096
|
+
nextRunAtMs: task.nextRunAtMs,
|
|
1097
|
+
record: task,
|
|
1098
|
+
runNowAtMs: task.runNowAtMs,
|
|
1099
|
+
status: task.status,
|
|
1100
|
+
teamId: task.destination.teamId,
|
|
1101
|
+
})
|
|
1102
|
+
.onConflictDoUpdate({
|
|
1103
|
+
target: juniorSchedulerTasks.id,
|
|
1104
|
+
set: {
|
|
1105
|
+
createdAtMs: sql`excluded.created_at_ms`,
|
|
1106
|
+
nextRunAtMs: sql`excluded.next_run_at_ms`,
|
|
1107
|
+
record: sql`excluded.record`,
|
|
1108
|
+
runNowAtMs: sql`excluded.run_now_at_ms`,
|
|
1109
|
+
status: sql`excluded.status`,
|
|
1110
|
+
teamId: sql`excluded.team_id`,
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1105
1113
|
}
|
|
1106
1114
|
|
|
1107
|
-
async function upsertSqlRun(db:
|
|
1108
|
-
await db
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
)
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
);
|
|
1115
|
+
async function upsertSqlRun(db: SchedulerDb, run: ScheduledRun): Promise<void> {
|
|
1116
|
+
await db
|
|
1117
|
+
.insert(juniorSchedulerRuns)
|
|
1118
|
+
.values({
|
|
1119
|
+
id: run.id,
|
|
1120
|
+
record: run,
|
|
1121
|
+
scheduledForMs: run.scheduledForMs,
|
|
1122
|
+
status: run.status,
|
|
1123
|
+
taskId: run.taskId,
|
|
1124
|
+
})
|
|
1125
|
+
.onConflictDoUpdate({
|
|
1126
|
+
target: juniorSchedulerRuns.id,
|
|
1127
|
+
set: {
|
|
1128
|
+
record: sql`excluded.record`,
|
|
1129
|
+
scheduledForMs: sql`excluded.scheduled_for_ms`,
|
|
1130
|
+
status: sql`excluded.status`,
|
|
1131
|
+
taskId: sql`excluded.task_id`,
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1127
1134
|
}
|
|
1128
1135
|
|
|
1129
1136
|
async function getTaskFromSql(
|
|
1130
|
-
db:
|
|
1137
|
+
db: SchedulerDb,
|
|
1131
1138
|
taskId: string,
|
|
1132
1139
|
): Promise<ScheduledTask | undefined> {
|
|
1133
|
-
const rows = await db
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1140
|
+
const rows = await db
|
|
1141
|
+
.select({ record: juniorSchedulerTasks.record })
|
|
1142
|
+
.from(juniorSchedulerTasks)
|
|
1143
|
+
.where(eq(juniorSchedulerTasks.id, taskId))
|
|
1144
|
+
.limit(1);
|
|
1137
1145
|
return rows[0] ? parseSqlTaskRow(rows[0]) : undefined;
|
|
1138
1146
|
}
|
|
1139
1147
|
|
|
1140
1148
|
async function getRunFromSql(
|
|
1141
|
-
db:
|
|
1142
|
-
runId: string,
|
|
1143
|
-
): Promise<ScheduledRun | undefined> {
|
|
1144
|
-
const rows = await db.query<SchedulerRunRow>(
|
|
1145
|
-
"SELECT record FROM junior_scheduler_runs WHERE id = $1",
|
|
1146
|
-
[runId],
|
|
1147
|
-
);
|
|
1148
|
-
return rows[0] ? parseSqlRunRow(rows[0]) : undefined;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
async function getClaimedRunSlotFromSql(
|
|
1152
|
-
db: PluginDb,
|
|
1149
|
+
db: SchedulerDb,
|
|
1153
1150
|
runId: string,
|
|
1154
1151
|
): Promise<ScheduledRun | undefined> {
|
|
1155
|
-
const rows = await db
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1152
|
+
const rows = await db
|
|
1153
|
+
.select({ record: juniorSchedulerRuns.record })
|
|
1154
|
+
.from(juniorSchedulerRuns)
|
|
1155
|
+
.where(eq(juniorSchedulerRuns.id, runId))
|
|
1156
|
+
.limit(1);
|
|
1159
1157
|
return rows[0] ? parseSqlRunRow(rows[0]) : undefined;
|
|
1160
1158
|
}
|
|
1161
1159
|
|
|
1162
|
-
async function listTasksFromSql(db:
|
|
1163
|
-
const rows = await db
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1160
|
+
async function listTasksFromSql(db: SchedulerDb): Promise<ScheduledTask[]> {
|
|
1161
|
+
const rows = await db
|
|
1162
|
+
.select({ record: juniorSchedulerTasks.record })
|
|
1163
|
+
.from(juniorSchedulerTasks)
|
|
1164
|
+
.where(ne(juniorSchedulerTasks.status, "deleted"))
|
|
1165
|
+
.orderBy(
|
|
1166
|
+
asc(juniorSchedulerTasks.createdAtMs),
|
|
1167
|
+
asc(juniorSchedulerTasks.id),
|
|
1168
|
+
);
|
|
1171
1169
|
return rows.map(parseSqlTaskRow).filter(present);
|
|
1172
1170
|
}
|
|
1173
1171
|
|
|
1174
1172
|
async function listTasksForTeamFromSql(
|
|
1175
|
-
db:
|
|
1173
|
+
db: SchedulerDb,
|
|
1176
1174
|
teamId: string,
|
|
1177
1175
|
): Promise<ScheduledTask[]> {
|
|
1178
|
-
const rows = await db
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1176
|
+
const rows = await db
|
|
1177
|
+
.select({ record: juniorSchedulerTasks.record })
|
|
1178
|
+
.from(juniorSchedulerTasks)
|
|
1179
|
+
.where(
|
|
1180
|
+
and(
|
|
1181
|
+
eq(juniorSchedulerTasks.teamId, teamId),
|
|
1182
|
+
ne(juniorSchedulerTasks.status, "deleted"),
|
|
1183
|
+
),
|
|
1184
|
+
)
|
|
1185
|
+
.orderBy(
|
|
1186
|
+
asc(juniorSchedulerTasks.createdAtMs),
|
|
1187
|
+
asc(juniorSchedulerTasks.id),
|
|
1188
|
+
);
|
|
1188
1189
|
return rows.map(parseSqlTaskRow).filter(present);
|
|
1189
1190
|
}
|
|
1190
1191
|
|
|
1191
1192
|
async function listIncompleteRunsForTasksFromSql(
|
|
1192
|
-
db:
|
|
1193
|
+
db: SchedulerDb,
|
|
1193
1194
|
tasks: ScheduledTask[],
|
|
1194
1195
|
): Promise<ScheduledRun[]> {
|
|
1195
1196
|
if (tasks.length === 0) {
|
|
1196
1197
|
return [];
|
|
1197
1198
|
}
|
|
1198
|
-
const rows = await db
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1199
|
+
const rows = await db
|
|
1200
|
+
.select({ record: juniorSchedulerRuns.record })
|
|
1201
|
+
.from(juniorSchedulerRuns)
|
|
1202
|
+
.where(
|
|
1203
|
+
and(
|
|
1204
|
+
inArray(
|
|
1205
|
+
juniorSchedulerRuns.taskId,
|
|
1206
|
+
tasks.map((task) => task.id),
|
|
1207
|
+
),
|
|
1208
|
+
inArray(juniorSchedulerRuns.status, [...SQL_INCOMPLETE_RUN_STATUSES]),
|
|
1209
|
+
),
|
|
1210
|
+
)
|
|
1211
|
+
.orderBy(
|
|
1212
|
+
asc(juniorSchedulerRuns.scheduledForMs),
|
|
1213
|
+
asc(juniorSchedulerRuns.id),
|
|
1214
|
+
);
|
|
1208
1215
|
return rows.map(parseSqlRunRow).filter(present);
|
|
1209
1216
|
}
|
|
1210
1217
|
|
|
1211
1218
|
class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
|
|
1212
|
-
constructor(private readonly db:
|
|
1219
|
+
constructor(private readonly db: SchedulerDb) {}
|
|
1213
1220
|
|
|
1214
1221
|
async saveTask(task: ScheduledTask): Promise<void> {
|
|
1215
1222
|
const next = requireStoredTask(task);
|
|
@@ -1220,7 +1227,7 @@ class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
|
|
|
1220
1227
|
}
|
|
1221
1228
|
|
|
1222
1229
|
private async saveTaskRecord(
|
|
1223
|
-
db:
|
|
1230
|
+
db: SchedulerDb,
|
|
1224
1231
|
task: ScheduledTask,
|
|
1225
1232
|
current: ScheduledTask | undefined,
|
|
1226
1233
|
): Promise<void> {
|
|
@@ -1232,10 +1239,14 @@ class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
|
|
|
1232
1239
|
typeof task.nextRunAtMs === "number" &&
|
|
1233
1240
|
Number.isFinite(task.nextRunAtMs)
|
|
1234
1241
|
) {
|
|
1235
|
-
await db
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1242
|
+
await db
|
|
1243
|
+
.delete(juniorSchedulerRuns)
|
|
1244
|
+
.where(
|
|
1245
|
+
and(
|
|
1246
|
+
eq(juniorSchedulerRuns.id, buildRunId(task.id, task.nextRunAtMs)),
|
|
1247
|
+
eq(juniorSchedulerRuns.status, "blocked"),
|
|
1248
|
+
),
|
|
1249
|
+
);
|
|
1239
1250
|
}
|
|
1240
1251
|
await upsertSqlTask(db, task);
|
|
1241
1252
|
}
|
|
@@ -1256,19 +1267,28 @@ class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
|
|
|
1256
1267
|
nowMs: number;
|
|
1257
1268
|
}): Promise<ScheduledRun | undefined> {
|
|
1258
1269
|
return await withSqlLock(this.db, "junior:scheduler:claim", async (db) => {
|
|
1259
|
-
const rows = await db
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1270
|
+
const rows = await db
|
|
1271
|
+
.select({ record: juniorSchedulerTasks.record })
|
|
1272
|
+
.from(juniorSchedulerTasks)
|
|
1273
|
+
.where(
|
|
1274
|
+
and(
|
|
1275
|
+
eq(juniorSchedulerTasks.status, "active"),
|
|
1276
|
+
or(
|
|
1277
|
+
and(
|
|
1278
|
+
isNotNull(juniorSchedulerTasks.runNowAtMs),
|
|
1279
|
+
lte(juniorSchedulerTasks.runNowAtMs, args.nowMs),
|
|
1280
|
+
),
|
|
1281
|
+
and(
|
|
1282
|
+
isNotNull(juniorSchedulerTasks.nextRunAtMs),
|
|
1283
|
+
lte(juniorSchedulerTasks.nextRunAtMs, args.nowMs),
|
|
1284
|
+
),
|
|
1285
|
+
),
|
|
1286
|
+
),
|
|
1287
|
+
)
|
|
1288
|
+
.orderBy(
|
|
1289
|
+
asc(juniorSchedulerTasks.createdAtMs),
|
|
1290
|
+
asc(juniorSchedulerTasks.id),
|
|
1291
|
+
);
|
|
1272
1292
|
|
|
1273
1293
|
for (const row of rows) {
|
|
1274
1294
|
const task = parseSqlTaskRow(row);
|
|
@@ -1302,7 +1322,7 @@ ORDER BY created_at_ms ASC, id ASC
|
|
|
1302
1322
|
await upsertSqlRun(db, reclaimed);
|
|
1303
1323
|
return reclaimed;
|
|
1304
1324
|
}
|
|
1305
|
-
const existingRun = await
|
|
1325
|
+
const existingRun = await getRunFromSql(db, runId);
|
|
1306
1326
|
if (existingRun) {
|
|
1307
1327
|
continue;
|
|
1308
1328
|
}
|
|
@@ -1330,7 +1350,7 @@ ORDER BY created_at_ms ASC, id ASC
|
|
|
1330
1350
|
}
|
|
1331
1351
|
|
|
1332
1352
|
private async skipMissedRun(
|
|
1333
|
-
db:
|
|
1353
|
+
db: SchedulerDb,
|
|
1334
1354
|
args: {
|
|
1335
1355
|
nowMs: number;
|
|
1336
1356
|
scheduledForMs: number;
|
|
@@ -1387,7 +1407,7 @@ ORDER BY created_at_ms ASC, id ASC
|
|
|
1387
1407
|
}
|
|
1388
1408
|
|
|
1389
1409
|
private async findStaleRecoveryCanonicalTask(
|
|
1390
|
-
db:
|
|
1410
|
+
db: SchedulerDb,
|
|
1391
1411
|
task: ScheduledTask,
|
|
1392
1412
|
): Promise<ScheduledTask | undefined> {
|
|
1393
1413
|
const fingerprint = taskDedupeFingerprint(task);
|
|
@@ -1625,20 +1645,20 @@ ORDER BY created_at_ms ASC, id ASC
|
|
|
1625
1645
|
}
|
|
1626
1646
|
|
|
1627
1647
|
/** Create a scheduler store backed by the plugin SQL database. */
|
|
1628
|
-
export function createSchedulerSqlStore(db:
|
|
1648
|
+
export function createSchedulerSqlStore(db: SchedulerDb): SchedulerStore {
|
|
1629
1649
|
return new SqlSchedulerStore(db);
|
|
1630
1650
|
}
|
|
1631
1651
|
|
|
1632
1652
|
/** Create a read-only scheduler operational store backed by SQL. */
|
|
1633
1653
|
export function createSchedulerOperationalSqlStore(
|
|
1634
|
-
db:
|
|
1654
|
+
db: SchedulerDb,
|
|
1635
1655
|
): SchedulerOperationalStore {
|
|
1636
1656
|
return new SqlSchedulerStore(db);
|
|
1637
1657
|
}
|
|
1638
1658
|
|
|
1639
1659
|
/** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
|
|
1640
1660
|
export async function migrateSchedulerStateToSql(args: {
|
|
1641
|
-
db:
|
|
1661
|
+
db: SchedulerDb;
|
|
1642
1662
|
state: PluginState;
|
|
1643
1663
|
}): Promise<{
|
|
1644
1664
|
existing: number;
|