chainlesschain 0.163.5 → 0.163.6
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/completions/_cc +1 -1
- package/completions/cc.bash +1 -1
- package/completions/cc.fish +1 -1
- package/completions/cc.ps1 +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-Cv2EE1Zy.js → AIOps-DDIUofA8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CcpxMLuW.js → ActionButton-Bm-5xYRn.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DE8mJoMI.js → Analytics-D5UH4ljI.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DKn9dk9L.js → AppLayout-D_ifjOOP.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-Cp3mG3Py.js → Artifacts-BY1mXdC7.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-zvsQfF2C.js → Audit-Bujr4Ml3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Bi85SsjN.js → BackgroundAgents-sioKSTcC.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cw47Dj6s.js → Backup-DDOAK8RK.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C5Ip2afH.js → BaseInput-CtCsVVke.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C0iAFkJv.js → Chat-DpNj0vnd.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BV9Wv7wO.js → ChatBubbleRenderer-CZta3Ftr.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-3KZZCiAF.js → Checkbox-DoHVNSXq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C8OMbBAz.js → Codegen-DlVl7Qmv.js} +1 -1
- package/src/assets/web-panel/assets/{Col-B-MB84LJ.js → Col-GIqoPYty.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DpR7Dxao.js → Community-CIAPEVji.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CyDtz0OS.js → Compact-BN7JiI_h.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-5tiRa2An.js → Compliance-CqUWCM9B.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DnQaKZ5x.js → Cowork-A4iPp7A4.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-BaWfoEDf.js → Cron-B2zeXXWb.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Bu0HoHhb.js → Crosschain-BR2xmWr8.js} +1 -1
- package/src/assets/web-panel/assets/{DID-31zk8upX.js → DID-BzFDjrIv.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BmVne-e2.js → Dashboard-DKtDo3yA.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B4PkoOWd.js → Dropdown-RNx43_YG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BZ6tyFtI.js → EmailListRenderer-SwMt4Rcy.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Osozn4Ds.js → FamilyGuardDashboard-CsYebZ4X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DaH8TOAP.js → Federation-BmRKvBTy.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-tbU0uwis.js → FormItemContext-CVxGTA-0.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Df8LQBrL.js → GenericCardRenderer-x2iXpyB7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DsMB6-14.js → Git-DfqbkiOP.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnH0dZzd.js → Governance-RuHyyj5O.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DhnN1N_P.js → Inference-Bn7uZ7rI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DnNshAgn.js → KnowledgeGraph-DlP7XefZ.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BmBFX3_N.js → Logs-BYNdOiCd.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DCslfQU3.js → MarkdownRenderer-aMtgN-R6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-CPEgWDc3.js → Marketplace-JQYmDqM8.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C36qjgF9.js → McpTools-Cyx4yZ-9.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-tNHvrgqN.js → Memory-VDuGZTN2.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Xaf6cKjc.js → MobileBridge-_yKo97gi.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-D2E5TTeO.js → MobileProjects-CXRXW3q7.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D4AJ2dwV.js → Mtc-acTGtfLH.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Df-gtn2_.js → MtcAudit-ClpjXfkY.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DTCHLWld.js → Multisig-_6Tn3o96.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DruZ_1Jb.js → NLProgramming-CK7pbZzP.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dx3pdGDs.js → Notes-Czy6V_P8.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Bi3L69oL.js → NotificationSettings-Cqx-UVv3.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BHY33m7C.js → OrderTableRenderer-C69WDxGy.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BG2ZVEV-.js → Organization-DFnA9vLQ.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Viwtg4Mn.js → Overflow-ReZkk2yT.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Clm_yc1Z.js → P2P-DzjSI0aI.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-NKmmDiOw.js → PdhVaultBrowser-DKTf5xIP.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DIL8u9Nk.js → Permissions-DhQ5klhR.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B-jM4wt-.js → PersonalDataHub-CUL1iHrW.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-627I__H5.js → Pipeline-DU88iu4N.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-2DN2qrkW.js → Privacy-Bw-sYfyK.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CN4wC0sF.js → ProjectInit-Bq3Q5y0z.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-K35vsh5H.js → ProjectSettings-C8spzpho.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Dd-xuPIc.js +1 -0
- package/src/assets/web-panel/assets/{Providers-MckaQUhQ.js → Providers-Bf4eTP9W.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BS3mtA_O.js → QrScannerModal-Dme9BDU1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BHp_nofL.js → QuickAsk-CIXfv1rE.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DlmnPuZj.js → Recommend-CIxvdeZ-.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-tsyFbvpy.js → RemoteSession-C6k7MExI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Ba1iWv2D.js → Reputation-CISDz2bl.js} +1 -1
- package/src/assets/web-panel/assets/{Row-NnlGC--f.js → Row-CdnDxXyf.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CjThSIRv.js → RssFeed-C-QpRHsl.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CojUyC5x.js → Search-DBjaGzSn.js} +1 -1
- package/src/assets/web-panel/assets/{Security-c-sxeJEc.js → Security-CQKukOeE.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CtcKEEmF.js → Services-DZz86jWb.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CIBBXflN.js → Skeleton-jb8RMK0d.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D1XOOK9n.js → Skills-CdY76fZG.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-hRfEp5Sn.js → Sla-C_CSiZX9.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-BJsSFGYy.js → SyncSettings-qvimGO0O.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CfSZ8A1L.js → Tasks-Dng5LPJC.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-idDf47W-.js → Templates-Bwqq5Wll.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-C-UsX6w6.js → Tenant-DNWuwP7Z.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BgJtX5n1.js → Terminal-B-6Othpp.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DP0BLKTl.js → TimelineRenderer-DKpKZfzx.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-LwCzqETp.js → Tokens-DzfGMd2J.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D_9tVch0.js → Trigger-BXwUi74J.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BH2UlEWh.js → Trust-BNzUq4ui.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CIpD0nYb.js → UkeySign-BYugxww4.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-9ShhYxK_.js → VideoEditing-DdauhtSJ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-C7afGNIG.js → Wallet-CSC9bCe_.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-bUKn6i5R.js → WebAuthn-Q8AN3bTO.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CxgQaGyp.js → WorkflowEditor-d-LBGCuA.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DMVkmJwx.js → chat-BLVDCgsZ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CDzQh5nS.js → colors-BZlPSEHl.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C51kEF4D.js → compact-item-IWseP6R6.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BTk9bXBn.js → createContext-DnVRV3Ao.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CMje33rb.js → hasIn-DLQZA_Wn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBz9LIMS.js → index-1jeAudHd.js} +1 -1
- package/src/assets/web-panel/assets/{index-hg_emBsi.js → index-BEu2lMBy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRG-aqOs.js → index-BOZJBTWZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDLkNkLj.js → index-BSjilZ9L.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvSeVzqe.js → index-BZs5bBnk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpcrFxMI.js → index-B_N4-Gq9.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9rC-fGW.js → index-Bbj2NS_I.js} +3 -3
- package/src/assets/web-panel/assets/{index-BVePe_Rf.js → index-BhA3QEni.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzCkNylB.js → index-Bxp7uDD8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAfY7m7i.js → index-C1ifZtXo.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTlsE8Df.js → index-C2FqfMvU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqU9njjT.js → index-C5dgnilC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtlEqHLP.js → index-CINJQEj6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeEaAEIp.js → index-COev0W2g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCLj9hcD.js → index-CT9vVN-X.js} +1 -1
- package/src/assets/web-panel/assets/{index-h31MwrME.js → index-CZDekj7T.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ld_4mFmC.js → index-CarMTIyQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwGPwjUu.js → index-CbQNL0ue.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5IWTMVl.js → index-CpBaJ3TD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BW4F5YDF.js → index-DB0MJgF_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CypsfcEq.js → index-DOoQpuQY.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfC-piWZ.js → index-DTbxp4UH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCRvG9lq.js → index-DUq0c7gq.js} +1 -1
- package/src/assets/web-panel/assets/{index-0Auljb10.js → index-DZpYeqrh.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4rQCS8T.js → index-D_LQodeJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtM8OT_7.js → index-DalA0TKC.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1oZQmAA.js → index-DjWnONdy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRLQyp2A.js → index-Dkwsi1Kn.js} +1 -1
- package/src/assets/web-panel/assets/{index-togc_uYn.js → index-DocEhinm.js} +1 -1
- package/src/assets/web-panel/assets/{index-x53Vf7la.js → index-DpVrK6fr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgivOKHA.js → index-Dy0R5ZED.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0-zMtKA.js → index-M7wxfAbm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXQBPLyE.js → index-N_GoWZX4.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1oZReaY.js → index-YSw_gxlZ.js} +1 -1
- package/src/assets/web-panel/assets/index-Zb2MeDSy.js +1 -0
- package/src/assets/web-panel/assets/index-fikqml4k.js +1 -0
- package/src/assets/web-panel/assets/{index-Cd0hmPxD.js → index-gt8bNefW.js} +1 -1
- package/src/assets/web-panel/assets/{index-OS-vfbTA.js → index-hwVDeoC0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJfnCuMq.js → index-jWb70ZgZ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-vrA8K-ac.js → initDefaultProps-Co67Kqmb.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ByEzwIAl.js → motion-BMOxQSF_.js} +1 -1
- package/src/assets/web-panel/assets/{move-Ck4AWjw-.js → move-C5fWDw-N.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-6p0l9SrB.js → mtc-parser-BJevpL0e.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B1CUPQ6R.js → omit-BCzNU8s6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B1STg1iW.js → pickAttrs-CybWvDPk.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DTTllkBV.js → placementArrow-BMWS9HnW.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-AIrrCfBb.js → responsiveObserve-BoL2UnGY.js} +1 -1
- package/src/assets/web-panel/assets/{slide-C_fPMjdR.js → slide-DWNfXraU.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-D5ow2yIP.js → statusUtils-BquTM3tX.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Dy3hEA--.js → styleChecker-BEkCleim.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-WfzcfSh4.js → useFlexGapSupport-Oecnq7EA.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CKSejfas.js → useFs-CkDffJ4F.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-od3zpPBN.js → usePersonalDataHub-qbvKXesB.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DRD_2DFA.js → vnode-C6v8n6mA.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmNrEtgf.js → zoom-4CTeFILV.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/command-manifest.json +1 -1
- package/src/commands/agenda.js +76 -4
- package/src/commands/automation.js +462 -3
- package/src/commands/background-session.js +102 -1
- package/src/commands/cowork.js +6 -1
- package/src/commands/loop.js +167 -36
- package/src/commands/routine.js +11 -4
- package/src/commands/scheduler-daemon.js +450 -0
- package/src/data/changelog.json +15 -2
- package/src/lib/agent-schedule-store.js +323 -43
- package/src/lib/automation-center-routines.js +363 -0
- package/src/lib/automation-center.js +435 -0
- package/src/lib/automation-engine.js +74 -19
- package/src/lib/automation-execution-authority.js +736 -0
- package/src/lib/background-agent-supervisor.js +78 -0
- package/src/lib/background-needs-input-incident.js +221 -0
- package/src/lib/cowork-cron.js +316 -18
- package/src/lib/cowork-observe.js +5 -1
- package/src/lib/loop.js +37 -2
- package/src/lib/permission-engine.js +14 -7
- package/src/lib/public-ide-capability-manifest.js +2 -0
- package/src/lib/routine-store.js +166 -65
- package/src/lib/scheduler-kernel/agenda-adapter.js +297 -27
- package/src/lib/scheduler-kernel/authority-resolver.js +163 -0
- package/src/lib/scheduler-kernel/automation-adapter.js +592 -0
- package/src/lib/scheduler-kernel/automation-event-adapter.js +721 -0
- package/src/lib/scheduler-kernel/contract.js +11 -3
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +58 -12
- package/src/lib/scheduler-kernel/loop-adapter.js +378 -0
- package/src/lib/scheduler-kernel/routine-adapter.js +303 -8
- package/src/lib/scheduler-kernel/service.js +328 -0
- package/src/lib/scheduler-kernel/store.js +828 -96
- package/src/runtime/agent-core.js +2 -1
- package/src/runtime/coding-agent-contract-shared.cjs +5 -0
- package/src/workers/background-agent-worker.js +91 -28
- package/src/assets/web-panel/assets/Projects-B_wQadhX.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-Dexb1f_6.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CdwPwYj8.js +0 -1
- package/src/assets/web-panel/assets/index-BEeOdfEe.js +0 -1
- package/src/assets/web-panel/assets/index-BTe0TOu6.js +0 -1
|
@@ -6,7 +6,6 @@ import { getHomeDir } from "../paths.js";
|
|
|
6
6
|
import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
|
|
7
7
|
import {
|
|
8
8
|
OCCURRENCE_STATUS,
|
|
9
|
-
SCHEDULER_SCHEMA_VERSION,
|
|
10
9
|
SchedulerKernelError,
|
|
11
10
|
canonicalJson,
|
|
12
11
|
deriveOccurrenceIdentity,
|
|
@@ -22,18 +21,35 @@ import {
|
|
|
22
21
|
const requireCjs = createRequire(import.meta.url);
|
|
23
22
|
|
|
24
23
|
export const SCHEDULER_APPLICATION_ID = 0x4343534b; // "CCSK"
|
|
24
|
+
export const SCHEDULER_STORE_SCHEMA_VERSION = 2;
|
|
25
25
|
export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
26
26
|
export const MAX_LEASE_MS = 24 * 60 * 60 * 1_000;
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
export const MIN_AUTHORITY_WINDOW_MS = 60_000;
|
|
28
|
+
export const MAX_AUTHORITY_WINDOW_MS = 31 * 24 * 60 * 60 * 1_000;
|
|
29
|
+
export const MAX_AUTHORITY_BUDGET = 1_000_000;
|
|
30
|
+
export const DEFAULT_AUTHORITY_WINDOW_MS = 24 * 60 * 60 * 1_000;
|
|
31
|
+
export const DEFAULT_AUTHORITY_MAX_RUNS = 100_000;
|
|
32
|
+
export const DEFAULT_AUTHORITY_MAX_UNITS = 100_000;
|
|
33
|
+
|
|
34
|
+
const MIGRATION_V1_NAME = "scheduler-kernel-v1";
|
|
35
|
+
const MIGRATION_V2_NAME = "scheduler-kernel-authority-v2";
|
|
36
|
+
const V1_USER_TABLES = Object.freeze([
|
|
37
|
+
"events",
|
|
38
|
+
"jobs",
|
|
39
|
+
"migrations",
|
|
40
|
+
"occurrences",
|
|
41
|
+
]);
|
|
29
42
|
const USER_TABLES = Object.freeze([
|
|
30
43
|
"events",
|
|
31
44
|
"jobs",
|
|
32
45
|
"migrations",
|
|
33
46
|
"occurrences",
|
|
47
|
+
"scheduler_authority_policies",
|
|
48
|
+
"scheduler_authority_reservations",
|
|
49
|
+
"scheduler_authority_usage",
|
|
34
50
|
]);
|
|
35
51
|
|
|
36
|
-
const MIGRATION_V1_SQL = `
|
|
52
|
+
export const MIGRATION_V1_SQL = `
|
|
37
53
|
CREATE TABLE migrations (
|
|
38
54
|
version INTEGER PRIMARY KEY NOT NULL,
|
|
39
55
|
name TEXT NOT NULL UNIQUE,
|
|
@@ -120,11 +136,86 @@ export const MIGRATION_V1_CHECKSUM = createHash("sha256")
|
|
|
120
136
|
.update(MIGRATION_V1_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
|
|
121
137
|
.digest("hex");
|
|
122
138
|
|
|
139
|
+
export const MIGRATION_V2_SQL = `
|
|
140
|
+
CREATE TABLE scheduler_authority_policies (
|
|
141
|
+
principal_type TEXT NOT NULL,
|
|
142
|
+
principal_id TEXT NOT NULL,
|
|
143
|
+
revision INTEGER NOT NULL CHECK (revision >= 1),
|
|
144
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
145
|
+
capabilities_json TEXT NOT NULL CHECK (json_valid(capabilities_json)),
|
|
146
|
+
window_ms INTEGER NOT NULL CHECK (
|
|
147
|
+
window_ms BETWEEN 60000 AND 2678400000
|
|
148
|
+
),
|
|
149
|
+
max_runs INTEGER NOT NULL CHECK (
|
|
150
|
+
max_runs BETWEEN 1 AND 1000000
|
|
151
|
+
),
|
|
152
|
+
max_units INTEGER NOT NULL CHECK (
|
|
153
|
+
max_units BETWEEN 1 AND 1000000
|
|
154
|
+
),
|
|
155
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
156
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
157
|
+
PRIMARY KEY (principal_type, principal_id)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE scheduler_authority_usage (
|
|
161
|
+
principal_type TEXT NOT NULL,
|
|
162
|
+
principal_id TEXT NOT NULL,
|
|
163
|
+
policy_revision INTEGER NOT NULL CHECK (policy_revision >= 1),
|
|
164
|
+
window_started_at INTEGER NOT NULL CHECK (window_started_at >= 0),
|
|
165
|
+
runs INTEGER NOT NULL CHECK (runs BETWEEN 0 AND 1000000),
|
|
166
|
+
units INTEGER NOT NULL CHECK (units BETWEEN 0 AND 1000000),
|
|
167
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
168
|
+
PRIMARY KEY (
|
|
169
|
+
principal_type,
|
|
170
|
+
principal_id,
|
|
171
|
+
policy_revision,
|
|
172
|
+
window_started_at
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE TABLE scheduler_authority_reservations (
|
|
177
|
+
occurrence_id TEXT PRIMARY KEY NOT NULL
|
|
178
|
+
REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
|
|
179
|
+
job_id TEXT NOT NULL REFERENCES jobs(job_id) ON DELETE RESTRICT,
|
|
180
|
+
principal_type TEXT NOT NULL,
|
|
181
|
+
principal_id TEXT NOT NULL,
|
|
182
|
+
policy_revision INTEGER NOT NULL CHECK (policy_revision >= 1),
|
|
183
|
+
window_started_at INTEGER NOT NULL CHECK (window_started_at >= 0),
|
|
184
|
+
units INTEGER NOT NULL CHECK (units BETWEEN 1 AND 1000000),
|
|
185
|
+
status TEXT NOT NULL CHECK (
|
|
186
|
+
status IN ('reserved', 'succeeded', 'failed')
|
|
187
|
+
),
|
|
188
|
+
outcome_json TEXT CHECK (
|
|
189
|
+
outcome_json IS NULL OR json_valid(outcome_json)
|
|
190
|
+
),
|
|
191
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
192
|
+
settled_at INTEGER,
|
|
193
|
+
CHECK (
|
|
194
|
+
(status = 'reserved' AND outcome_json IS NULL AND settled_at IS NULL)
|
|
195
|
+
OR
|
|
196
|
+
(status <> 'reserved' AND outcome_json IS NOT NULL AND settled_at IS NOT NULL)
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE INDEX scheduler_authority_reservations_principal
|
|
201
|
+
ON scheduler_authority_reservations
|
|
202
|
+
(principal_type, principal_id, policy_revision, window_started_at);
|
|
203
|
+
CREATE INDEX scheduler_authority_reservations_status
|
|
204
|
+
ON scheduler_authority_reservations(status, created_at);
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
export const MIGRATION_V2_CHECKSUM = createHash("sha256")
|
|
208
|
+
.update(MIGRATION_V2_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
|
|
209
|
+
.update("\0scheduler-authority-v2-backfill-v1", "utf8")
|
|
210
|
+
.digest("hex");
|
|
211
|
+
|
|
123
212
|
// Fingerprint of the normalized sqlite_master catalog produced by the v1 DDL.
|
|
124
213
|
// Unlike the migration-source checksum, this also detects added triggers/views
|
|
125
214
|
// and constraint/foreign-key changes that preserve the visible column list.
|
|
126
215
|
export const SCHEMA_V1_FINGERPRINT =
|
|
127
216
|
"cf244f675ac7430683f67046b57524eb6c14baa45d6463ed3f704be898fec887";
|
|
217
|
+
export const SCHEMA_V2_FINGERPRINT =
|
|
218
|
+
"402c58d4b3b217699591528f9be6c1c7fe341650b843eb2d8d9d907779ff27b0";
|
|
128
219
|
|
|
129
220
|
const EXPECTED_COLUMNS = Object.freeze({
|
|
130
221
|
migrations: [
|
|
@@ -177,9 +268,45 @@ const EXPECTED_COLUMNS = Object.freeze({
|
|
|
177
268
|
["fence", "INTEGER", 0, 0],
|
|
178
269
|
["data_json", "TEXT", 1, 0],
|
|
179
270
|
],
|
|
271
|
+
scheduler_authority_policies: [
|
|
272
|
+
["principal_type", "TEXT", 1, 1],
|
|
273
|
+
["principal_id", "TEXT", 1, 2],
|
|
274
|
+
["revision", "INTEGER", 1, 0],
|
|
275
|
+
["enabled", "INTEGER", 1, 0],
|
|
276
|
+
["capabilities_json", "TEXT", 1, 0],
|
|
277
|
+
["window_ms", "INTEGER", 1, 0],
|
|
278
|
+
["max_runs", "INTEGER", 1, 0],
|
|
279
|
+
["max_units", "INTEGER", 1, 0],
|
|
280
|
+
["created_at", "INTEGER", 1, 0],
|
|
281
|
+
["updated_at", "INTEGER", 1, 0],
|
|
282
|
+
],
|
|
283
|
+
scheduler_authority_usage: [
|
|
284
|
+
["principal_type", "TEXT", 1, 1],
|
|
285
|
+
["principal_id", "TEXT", 1, 2],
|
|
286
|
+
["policy_revision", "INTEGER", 1, 3],
|
|
287
|
+
["window_started_at", "INTEGER", 1, 4],
|
|
288
|
+
["runs", "INTEGER", 1, 0],
|
|
289
|
+
["units", "INTEGER", 1, 0],
|
|
290
|
+
["updated_at", "INTEGER", 1, 0],
|
|
291
|
+
],
|
|
292
|
+
scheduler_authority_reservations: [
|
|
293
|
+
["occurrence_id", "TEXT", 1, 1],
|
|
294
|
+
["job_id", "TEXT", 1, 0],
|
|
295
|
+
["principal_type", "TEXT", 1, 0],
|
|
296
|
+
["principal_id", "TEXT", 1, 0],
|
|
297
|
+
["policy_revision", "INTEGER", 1, 0],
|
|
298
|
+
["window_started_at", "INTEGER", 1, 0],
|
|
299
|
+
["units", "INTEGER", 1, 0],
|
|
300
|
+
["status", "TEXT", 1, 0],
|
|
301
|
+
["outcome_json", "TEXT", 0, 0],
|
|
302
|
+
["created_at", "INTEGER", 1, 0],
|
|
303
|
+
["settled_at", "INTEGER", 0, 0],
|
|
304
|
+
],
|
|
180
305
|
});
|
|
181
306
|
|
|
182
307
|
const EXPECTED_INDEXES = Object.freeze([
|
|
308
|
+
"scheduler_authority_reservations_principal",
|
|
309
|
+
"scheduler_authority_reservations_status",
|
|
183
310
|
"scheduler_events_job",
|
|
184
311
|
"scheduler_events_occurrence",
|
|
185
312
|
"scheduler_occurrences_claim",
|
|
@@ -218,6 +345,84 @@ function normalizeBoolean(value, field) {
|
|
|
218
345
|
return value;
|
|
219
346
|
}
|
|
220
347
|
|
|
348
|
+
function normalizeAuthorityBudgetLimit(
|
|
349
|
+
value,
|
|
350
|
+
field,
|
|
351
|
+
{ minimum = 1, maximum = MAX_AUTHORITY_BUDGET } = {},
|
|
352
|
+
) {
|
|
353
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
354
|
+
throw invalidArgument(
|
|
355
|
+
`${field} must be an integer between ${minimum} and ${maximum}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizeAuthorityPrincipal(principal) {
|
|
362
|
+
if (!principal || typeof principal !== "object" || Array.isArray(principal)) {
|
|
363
|
+
throw invalidArgument("authority principal must be an object");
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
type: normalizeIdentifier(principal.type, "authority.principal.type", {
|
|
367
|
+
maxLength: 64,
|
|
368
|
+
}),
|
|
369
|
+
id: normalizeIdentifier(principal.id, "authority.principal.id"),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function normalizeCapabilityList(value, field = "capabilities") {
|
|
374
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
|
|
375
|
+
throw invalidArgument(`${field} must contain between 1 and 256 entries`);
|
|
376
|
+
}
|
|
377
|
+
const capabilities = [
|
|
378
|
+
...new Set(
|
|
379
|
+
value.map((capability, index) =>
|
|
380
|
+
normalizeIdentifier(capability, `${field}[${index}]`),
|
|
381
|
+
),
|
|
382
|
+
),
|
|
383
|
+
].sort();
|
|
384
|
+
if (capabilities.includes("*")) {
|
|
385
|
+
throw invalidArgument(`${field} must contain exact capabilities`);
|
|
386
|
+
}
|
|
387
|
+
return capabilities;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function mapAuthorityPolicy(row) {
|
|
391
|
+
if (!row) return null;
|
|
392
|
+
return {
|
|
393
|
+
principal: { type: row.principal_type, id: row.principal_id },
|
|
394
|
+
revision: row.revision,
|
|
395
|
+
enabled: row.enabled === 1,
|
|
396
|
+
capabilities: normalizeCapabilityList(
|
|
397
|
+
readStoredJson(row.capabilities_json, "scheduler authority capabilities"),
|
|
398
|
+
),
|
|
399
|
+
windowMs: row.window_ms,
|
|
400
|
+
maxRuns: row.max_runs,
|
|
401
|
+
maxUnits: row.max_units,
|
|
402
|
+
createdAt: row.created_at,
|
|
403
|
+
updatedAt: row.updated_at,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function mapAuthorityReservation(row) {
|
|
408
|
+
if (!row) return null;
|
|
409
|
+
return {
|
|
410
|
+
occurrenceId: row.occurrence_id,
|
|
411
|
+
jobId: row.job_id,
|
|
412
|
+
principal: { type: row.principal_type, id: row.principal_id },
|
|
413
|
+
policyRevision: row.policy_revision,
|
|
414
|
+
windowStartedAt: row.window_started_at,
|
|
415
|
+
units: row.units,
|
|
416
|
+
status: row.status,
|
|
417
|
+
outcome:
|
|
418
|
+
row.outcome_json === null
|
|
419
|
+
? null
|
|
420
|
+
: readStoredJson(row.outcome_json, "scheduler authority outcome"),
|
|
421
|
+
createdAt: row.created_at,
|
|
422
|
+
settledAt: row.settled_at,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
221
426
|
function normalizeJobInput(input) {
|
|
222
427
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
223
428
|
throw invalidArgument("job must be an object");
|
|
@@ -428,7 +633,7 @@ function verifyTableShape(db, table) {
|
|
|
428
633
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
429
634
|
throw schemaError(
|
|
430
635
|
"SCHEDULER_SCHEMA_CORRUPT",
|
|
431
|
-
`Scheduler table shape
|
|
636
|
+
`Scheduler table shape is invalid: ${table}`,
|
|
432
637
|
undefined,
|
|
433
638
|
{ expected, actual },
|
|
434
639
|
);
|
|
@@ -454,15 +659,16 @@ function schemaFingerprint(db) {
|
|
|
454
659
|
.digest("hex");
|
|
455
660
|
}
|
|
456
661
|
|
|
457
|
-
function verifySchema(db) {
|
|
662
|
+
function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
|
|
458
663
|
assertDatabaseIntegrity(db);
|
|
459
664
|
const tables = listUserTables(db);
|
|
460
|
-
|
|
665
|
+
const expectedTables = version === 1 ? V1_USER_TABLES : USER_TABLES;
|
|
666
|
+
if (JSON.stringify(tables) !== JSON.stringify(expectedTables)) {
|
|
461
667
|
throw schemaError(
|
|
462
668
|
"SCHEDULER_SCHEMA_UNKNOWN",
|
|
463
669
|
"Scheduler database has unknown or missing tables",
|
|
464
670
|
undefined,
|
|
465
|
-
{ expected:
|
|
671
|
+
{ expected: expectedTables, actual: tables },
|
|
466
672
|
);
|
|
467
673
|
}
|
|
468
674
|
|
|
@@ -478,10 +684,7 @@ function verifySchema(db) {
|
|
|
478
684
|
cause,
|
|
479
685
|
);
|
|
480
686
|
}
|
|
481
|
-
if (
|
|
482
|
-
applicationId !== SCHEDULER_APPLICATION_ID ||
|
|
483
|
-
userVersion !== SCHEDULER_SCHEMA_VERSION
|
|
484
|
-
) {
|
|
687
|
+
if (applicationId !== SCHEDULER_APPLICATION_ID || userVersion !== version) {
|
|
485
688
|
throw schemaError(
|
|
486
689
|
"SCHEDULER_SCHEMA_UNKNOWN",
|
|
487
690
|
"Scheduler schema version or application identity is unknown",
|
|
@@ -490,16 +693,18 @@ function verifySchema(db) {
|
|
|
490
693
|
);
|
|
491
694
|
}
|
|
492
695
|
|
|
493
|
-
for (const table of
|
|
696
|
+
for (const table of expectedTables) verifyTableShape(db, table);
|
|
494
697
|
|
|
495
698
|
const actualFingerprint = schemaFingerprint(db);
|
|
496
|
-
|
|
699
|
+
const expectedFingerprint =
|
|
700
|
+
version === 1 ? SCHEMA_V1_FINGERPRINT : SCHEMA_V2_FINGERPRINT;
|
|
701
|
+
if (actualFingerprint !== expectedFingerprint) {
|
|
497
702
|
throw schemaError(
|
|
498
703
|
"SCHEDULER_SCHEMA_CORRUPT",
|
|
499
|
-
|
|
704
|
+
`Scheduler sqlite_master catalog does not match v${version}`,
|
|
500
705
|
undefined,
|
|
501
706
|
{
|
|
502
|
-
expected:
|
|
707
|
+
expected: expectedFingerprint,
|
|
503
708
|
actual: actualFingerprint,
|
|
504
709
|
},
|
|
505
710
|
);
|
|
@@ -520,10 +725,14 @@ function verifySchema(db) {
|
|
|
520
725
|
);
|
|
521
726
|
}
|
|
522
727
|
if (
|
|
523
|
-
migrations.length !==
|
|
524
|
-
migrations[0].version !==
|
|
525
|
-
migrations[0].name !==
|
|
526
|
-
migrations[0].checksum !== MIGRATION_V1_CHECKSUM
|
|
728
|
+
migrations.length !== version ||
|
|
729
|
+
migrations[0].version !== 1 ||
|
|
730
|
+
migrations[0].name !== MIGRATION_V1_NAME ||
|
|
731
|
+
migrations[0].checksum !== MIGRATION_V1_CHECKSUM ||
|
|
732
|
+
(version === 2 &&
|
|
733
|
+
(migrations[1]?.version !== 2 ||
|
|
734
|
+
migrations[1]?.name !== MIGRATION_V2_NAME ||
|
|
735
|
+
migrations[1]?.checksum !== MIGRATION_V2_CHECKSUM))
|
|
527
736
|
) {
|
|
528
737
|
throw schemaError(
|
|
529
738
|
"SCHEDULER_SCHEMA_UNKNOWN",
|
|
@@ -539,12 +748,18 @@ function verifySchema(db) {
|
|
|
539
748
|
)
|
|
540
749
|
.all()
|
|
541
750
|
.map((row) => row.name);
|
|
542
|
-
|
|
751
|
+
const expectedIndexes =
|
|
752
|
+
version === 1
|
|
753
|
+
? EXPECTED_INDEXES.filter(
|
|
754
|
+
(name) => !name.startsWith("scheduler_authority_"),
|
|
755
|
+
)
|
|
756
|
+
: EXPECTED_INDEXES;
|
|
757
|
+
if (JSON.stringify(indexes) !== JSON.stringify(expectedIndexes)) {
|
|
543
758
|
throw schemaError(
|
|
544
759
|
"SCHEDULER_SCHEMA_CORRUPT",
|
|
545
|
-
|
|
760
|
+
`Scheduler indexes do not match v${version}`,
|
|
546
761
|
undefined,
|
|
547
|
-
{ expected:
|
|
762
|
+
{ expected: expectedIndexes, actual: indexes },
|
|
548
763
|
);
|
|
549
764
|
}
|
|
550
765
|
const foreignKeyFailures = db.pragma("foreign_key_check");
|
|
@@ -558,22 +773,108 @@ function verifySchema(db) {
|
|
|
558
773
|
}
|
|
559
774
|
}
|
|
560
775
|
|
|
776
|
+
function migrateAuthorityV2(db, now) {
|
|
777
|
+
db.exec(MIGRATION_V2_SQL);
|
|
778
|
+
const policies = new Map();
|
|
779
|
+
const jobs = db
|
|
780
|
+
.prepare("SELECT job_id, authority_json FROM jobs ORDER BY job_id")
|
|
781
|
+
.all();
|
|
782
|
+
const occurrences = db
|
|
783
|
+
.prepare(
|
|
784
|
+
`SELECT occurrence_id, status, authority_json
|
|
785
|
+
FROM occurrences ORDER BY occurrence_id`,
|
|
786
|
+
)
|
|
787
|
+
.all();
|
|
788
|
+
const bind = (encoded, field) => {
|
|
789
|
+
const authority = readStoredAuthority(encoded, field);
|
|
790
|
+
const key = canonicalJson(authority.principal, `${field}.principal`);
|
|
791
|
+
const current = policies.get(key) ?? {
|
|
792
|
+
principal: authority.principal,
|
|
793
|
+
capabilities: new Set(),
|
|
794
|
+
};
|
|
795
|
+
for (const capability of authority.requestedCapabilities) {
|
|
796
|
+
current.capabilities.add(capability);
|
|
797
|
+
}
|
|
798
|
+
policies.set(key, current);
|
|
799
|
+
return {
|
|
800
|
+
...authority,
|
|
801
|
+
authorizationRefs: {
|
|
802
|
+
...authority.authorizationRefs,
|
|
803
|
+
schedulerPolicyRevision: "scheduler-authority:1",
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
};
|
|
807
|
+
const boundJobs = jobs.map((row) => ({
|
|
808
|
+
id: row.job_id,
|
|
809
|
+
authority: bind(row.authority_json, `job ${row.job_id} authority`),
|
|
810
|
+
}));
|
|
811
|
+
const boundOccurrences = occurrences.map((row) => {
|
|
812
|
+
const field = `occurrence ${row.occurrence_id} authority`;
|
|
813
|
+
const terminal =
|
|
814
|
+
row.status === OCCURRENCE_STATUS.SUCCEEDED ||
|
|
815
|
+
row.status === OCCURRENCE_STATUS.DEAD_LETTER;
|
|
816
|
+
return {
|
|
817
|
+
id: row.occurrence_id,
|
|
818
|
+
terminal,
|
|
819
|
+
// Validate terminal history too, but do not widen the active policy with
|
|
820
|
+
// capabilities that no current or replayable occurrence still needs.
|
|
821
|
+
authority: terminal
|
|
822
|
+
? readStoredAuthority(row.authority_json, field)
|
|
823
|
+
: bind(row.authority_json, field),
|
|
824
|
+
};
|
|
825
|
+
});
|
|
826
|
+
const insertPolicy = db.prepare(
|
|
827
|
+
`INSERT INTO scheduler_authority_policies
|
|
828
|
+
(principal_type, principal_id, revision, enabled,
|
|
829
|
+
capabilities_json, window_ms, max_runs, max_units,
|
|
830
|
+
created_at, updated_at)
|
|
831
|
+
VALUES (?, ?, 1, 1, ?, ?, ?, ?, ?, ?)`,
|
|
832
|
+
);
|
|
833
|
+
for (const { principal, capabilities } of policies.values()) {
|
|
834
|
+
insertPolicy.run(
|
|
835
|
+
principal.type,
|
|
836
|
+
principal.id,
|
|
837
|
+
canonicalJson([...capabilities].sort(), "authorityPolicy.capabilities"),
|
|
838
|
+
DEFAULT_AUTHORITY_WINDOW_MS,
|
|
839
|
+
DEFAULT_AUTHORITY_MAX_RUNS,
|
|
840
|
+
DEFAULT_AUTHORITY_MAX_UNITS,
|
|
841
|
+
now,
|
|
842
|
+
now,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const updateJob = db.prepare(
|
|
846
|
+
"UPDATE jobs SET authority_json = ? WHERE job_id = ?",
|
|
847
|
+
);
|
|
848
|
+
for (const row of boundJobs) {
|
|
849
|
+
updateJob.run(canonicalJson(row.authority, "job.authority"), row.id);
|
|
850
|
+
}
|
|
851
|
+
const updateOccurrence = db.prepare(
|
|
852
|
+
"UPDATE occurrences SET authority_json = ? WHERE occurrence_id = ?",
|
|
853
|
+
);
|
|
854
|
+
for (const row of boundOccurrences) {
|
|
855
|
+
if (row.terminal) continue;
|
|
856
|
+
updateOccurrence.run(
|
|
857
|
+
canonicalJson(row.authority, "occurrence.authority"),
|
|
858
|
+
row.id,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
561
863
|
function initializeOrVerifySchema(db, now) {
|
|
562
864
|
assertDatabaseIntegrity(db);
|
|
563
865
|
const tables = listUserTables(db);
|
|
564
866
|
if (tables.length === 0) {
|
|
565
867
|
const migrate = db.transaction(() => {
|
|
566
868
|
db.exec(MIGRATION_V1_SQL);
|
|
869
|
+
migrateAuthorityV2(db, now);
|
|
567
870
|
db.prepare(
|
|
568
871
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
569
|
-
).run(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
now,
|
|
574
|
-
);
|
|
872
|
+
).run(1, MIGRATION_V1_NAME, MIGRATION_V1_CHECKSUM, now);
|
|
873
|
+
db.prepare(
|
|
874
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
875
|
+
).run(2, MIGRATION_V2_NAME, MIGRATION_V2_CHECKSUM, now);
|
|
575
876
|
db.pragma(`application_id = ${SCHEDULER_APPLICATION_ID}`);
|
|
576
|
-
db.pragma(`user_version = ${
|
|
877
|
+
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
577
878
|
});
|
|
578
879
|
migrate.immediate();
|
|
579
880
|
} else if (!tables.includes("migrations")) {
|
|
@@ -583,6 +884,16 @@ function initializeOrVerifySchema(db, now) {
|
|
|
583
884
|
undefined,
|
|
584
885
|
{ tables },
|
|
585
886
|
);
|
|
887
|
+
} else if (db.pragma("user_version", { simple: true }) === 1) {
|
|
888
|
+
verifySchema(db, 1);
|
|
889
|
+
const migrate = db.transaction(() => {
|
|
890
|
+
migrateAuthorityV2(db, now);
|
|
891
|
+
db.prepare(
|
|
892
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
893
|
+
).run(2, MIGRATION_V2_NAME, MIGRATION_V2_CHECKSUM, now);
|
|
894
|
+
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
895
|
+
});
|
|
896
|
+
migrate.immediate();
|
|
586
897
|
}
|
|
587
898
|
verifySchema(db);
|
|
588
899
|
}
|
|
@@ -683,6 +994,12 @@ export class SchedulerStore {
|
|
|
683
994
|
getOccurrenceByKey: db.prepare(
|
|
684
995
|
"SELECT * FROM occurrences WHERE idempotency_key = ?",
|
|
685
996
|
),
|
|
997
|
+
listOccurrencesByTrigger: db.prepare(
|
|
998
|
+
`SELECT * FROM occurrences
|
|
999
|
+
WHERE job_id = ? AND trigger_key = ?
|
|
1000
|
+
ORDER BY created_at ASC, occurrence_id ASC
|
|
1001
|
+
LIMIT ?`,
|
|
1002
|
+
),
|
|
686
1003
|
insertEvent: db.prepare(`
|
|
687
1004
|
INSERT INTO events
|
|
688
1005
|
(job_id, occurrence_id, event_type, occurred_at, owner_id, fence, data_json)
|
|
@@ -855,83 +1172,136 @@ export class SchedulerStore {
|
|
|
855
1172
|
});
|
|
856
1173
|
}
|
|
857
1174
|
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1175
|
+
_enqueueOccurrence({ id, scheduled, key, available, payload, now }) {
|
|
1176
|
+
const job = mapJob(this.statements.getJob.get(id));
|
|
1177
|
+
if (!job) {
|
|
1178
|
+
throw new SchedulerKernelError(
|
|
1179
|
+
"SCHEDULER_NOT_FOUND",
|
|
1180
|
+
`Scheduler job does not exist: ${id}`,
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
if (!job.enabled) {
|
|
1184
|
+
throw new SchedulerKernelError(
|
|
1185
|
+
"SCHEDULER_JOB_DISABLED",
|
|
1186
|
+
`Scheduler job is disabled: ${id}`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
const identity = deriveOccurrenceIdentity({
|
|
1190
|
+
jobId: id,
|
|
1191
|
+
jobRevision: job.revision,
|
|
1192
|
+
scheduledFor: scheduled,
|
|
1193
|
+
triggerKey: key,
|
|
1194
|
+
});
|
|
1195
|
+
const payloadJson = canonicalJson(
|
|
1196
|
+
payload === undefined ? job.payload : payload,
|
|
1197
|
+
"occurrence.payload",
|
|
1198
|
+
);
|
|
1199
|
+
const result = this.db
|
|
1200
|
+
.prepare(
|
|
1201
|
+
`
|
|
1202
|
+
INSERT INTO occurrences
|
|
1203
|
+
(occurrence_id, job_id, job_revision, idempotency_key, trigger_key,
|
|
1204
|
+
scheduled_for, available_at, status, attempt, max_attempts, fence,
|
|
1205
|
+
lease_owner, lease_expires_at, authority_json, payload_json,
|
|
1206
|
+
last_error_json, result_json, created_at, updated_at, settled_at)
|
|
1207
|
+
VALUES
|
|
1208
|
+
(@occurrenceId, @jobId, @jobRevision, @idempotencyKey, @triggerKey,
|
|
1209
|
+
@scheduledFor, @availableAt, 'queued', 0, @maxAttempts, 0,
|
|
1210
|
+
NULL, NULL, @authorityJson, @payloadJson,
|
|
1211
|
+
NULL, NULL, @now, @now, NULL)
|
|
1212
|
+
ON CONFLICT(idempotency_key) DO NOTHING
|
|
1213
|
+
`,
|
|
1214
|
+
)
|
|
1215
|
+
.run({
|
|
1216
|
+
...identity,
|
|
879
1217
|
jobId: id,
|
|
880
1218
|
jobRevision: job.revision,
|
|
881
|
-
scheduledFor: scheduled,
|
|
882
1219
|
triggerKey: key,
|
|
1220
|
+
scheduledFor: scheduled,
|
|
1221
|
+
availableAt: available,
|
|
1222
|
+
maxAttempts: job.maxAttempts,
|
|
1223
|
+
authorityJson: canonicalJson(job.authority, "occurrence.authority"),
|
|
1224
|
+
payloadJson,
|
|
1225
|
+
now,
|
|
883
1226
|
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1227
|
+
const row = this.statements.getOccurrenceByKey.get(identity.idempotencyKey);
|
|
1228
|
+
if (!row || row.occurrence_id !== identity.occurrenceId) {
|
|
1229
|
+
throw schemaError(
|
|
1230
|
+
"SCHEDULER_DATA_CORRUPT",
|
|
1231
|
+
"Occurrence idempotency identity does not match stored data",
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (row.payload_json !== payloadJson) {
|
|
1235
|
+
throw new SchedulerKernelError(
|
|
1236
|
+
"SCHEDULER_IDEMPOTENCY_PAYLOAD_MISMATCH",
|
|
1237
|
+
"Occurrence idempotency identity was reused with a different payload",
|
|
1238
|
+
{ jobId: id, triggerKey: key },
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
if (result.changes === 1) {
|
|
1242
|
+
this._appendEvent({
|
|
1243
|
+
jobId: id,
|
|
1244
|
+
occurrenceId: identity.occurrenceId,
|
|
1245
|
+
type: "occurrence_enqueued",
|
|
1246
|
+
occurredAt: now,
|
|
1247
|
+
data: {
|
|
903
1248
|
jobRevision: job.revision,
|
|
904
|
-
triggerKey: key,
|
|
905
1249
|
scheduledFor: scheduled,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1250
|
+
triggerKey: key,
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
return { ...mapOccurrence(row), deduplicated: result.changes === 0 };
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
enqueueOccurrence({
|
|
1258
|
+
jobId,
|
|
1259
|
+
scheduledFor,
|
|
1260
|
+
triggerKey,
|
|
1261
|
+
availableAt,
|
|
1262
|
+
payload,
|
|
1263
|
+
} = {}) {
|
|
1264
|
+
const id = normalizeIdentifier(jobId, "jobId");
|
|
1265
|
+
const scheduled = normalizeEpochMs(scheduledFor, "scheduledFor");
|
|
1266
|
+
const key = normalizeIdentifier(triggerKey, "triggerKey");
|
|
1267
|
+
const available = normalizeEpochMs(availableAt ?? scheduled, "availableAt");
|
|
1268
|
+
const now = this._now();
|
|
1269
|
+
return this._write(() =>
|
|
1270
|
+
this._enqueueOccurrence({ id, scheduled, key, available, payload, now }),
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
enqueueOccurrenceOncePerTrigger({
|
|
1275
|
+
jobId,
|
|
1276
|
+
scheduledFor,
|
|
1277
|
+
triggerKey,
|
|
1278
|
+
availableAt,
|
|
1279
|
+
payload,
|
|
1280
|
+
} = {}) {
|
|
1281
|
+
const id = normalizeIdentifier(jobId, "jobId");
|
|
1282
|
+
const scheduled = normalizeEpochMs(scheduledFor, "scheduledFor");
|
|
1283
|
+
const key = normalizeIdentifier(triggerKey, "triggerKey");
|
|
1284
|
+
const available = normalizeEpochMs(availableAt ?? scheduled, "availableAt");
|
|
1285
|
+
const now = this._now();
|
|
1286
|
+
return this._write(() => {
|
|
1287
|
+
const prior = this.statements.listOccurrencesByTrigger.all(id, key, 2);
|
|
1288
|
+
if (prior.length > 1) {
|
|
916
1289
|
throw schemaError(
|
|
917
1290
|
"SCHEDULER_DATA_CORRUPT",
|
|
918
|
-
"
|
|
1291
|
+
"Multiple occurrences exist for a unique job trigger",
|
|
919
1292
|
);
|
|
920
1293
|
}
|
|
921
|
-
if (
|
|
922
|
-
|
|
923
|
-
jobId: id,
|
|
924
|
-
occurrenceId: identity.occurrenceId,
|
|
925
|
-
type: "occurrence_enqueued",
|
|
926
|
-
occurredAt: now,
|
|
927
|
-
data: {
|
|
928
|
-
jobRevision: job.revision,
|
|
929
|
-
scheduledFor: scheduled,
|
|
930
|
-
triggerKey: key,
|
|
931
|
-
},
|
|
932
|
-
});
|
|
1294
|
+
if (prior.length === 1) {
|
|
1295
|
+
return { ...mapOccurrence(prior[0]), deduplicated: true };
|
|
933
1296
|
}
|
|
934
|
-
return {
|
|
1297
|
+
return this._enqueueOccurrence({
|
|
1298
|
+
id,
|
|
1299
|
+
scheduled,
|
|
1300
|
+
key,
|
|
1301
|
+
available,
|
|
1302
|
+
payload,
|
|
1303
|
+
now,
|
|
1304
|
+
});
|
|
935
1305
|
});
|
|
936
1306
|
}
|
|
937
1307
|
|
|
@@ -944,6 +1314,19 @@ export class SchedulerStore {
|
|
|
944
1314
|
);
|
|
945
1315
|
}
|
|
946
1316
|
|
|
1317
|
+
listOccurrencesByTrigger({ jobId, triggerKey, limit = 2 } = {}) {
|
|
1318
|
+
this._assertOpen();
|
|
1319
|
+
const id = normalizeIdentifier(jobId, "jobId");
|
|
1320
|
+
const key = normalizeIdentifier(triggerKey, "triggerKey");
|
|
1321
|
+
const boundedLimit = Math.min(
|
|
1322
|
+
200,
|
|
1323
|
+
Math.max(1, Number.isSafeInteger(Number(limit)) ? Number(limit) : 2),
|
|
1324
|
+
);
|
|
1325
|
+
return this.statements.listOccurrencesByTrigger
|
|
1326
|
+
.all(id, key, boundedLimit)
|
|
1327
|
+
.map(mapOccurrence);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
947
1330
|
_deadLetterExpiredLeases(now) {
|
|
948
1331
|
const expired = this.db
|
|
949
1332
|
.prepare(
|
|
@@ -1353,6 +1736,42 @@ export class SchedulerStore {
|
|
|
1353
1736
|
settledAt,
|
|
1354
1737
|
});
|
|
1355
1738
|
if (update.changes !== 1) throw leaseLost(id);
|
|
1739
|
+
const reservation = this.db
|
|
1740
|
+
.prepare(
|
|
1741
|
+
`SELECT * FROM scheduler_authority_reservations
|
|
1742
|
+
WHERE occurrence_id = ?`,
|
|
1743
|
+
)
|
|
1744
|
+
.get(id);
|
|
1745
|
+
if (
|
|
1746
|
+
reservation?.status === "reserved" &&
|
|
1747
|
+
(nextStatus === OCCURRENCE_STATUS.SUCCEEDED ||
|
|
1748
|
+
nextStatus === OCCURRENCE_STATUS.DEAD_LETTER)
|
|
1749
|
+
) {
|
|
1750
|
+
const authorityOutcome =
|
|
1751
|
+
nextStatus === OCCURRENCE_STATUS.SUCCEEDED ? "succeeded" : "failed";
|
|
1752
|
+
const authorityResult =
|
|
1753
|
+
authorityOutcome === "succeeded"
|
|
1754
|
+
? { status: nextStatus, result: normalizedResult }
|
|
1755
|
+
: { status: nextStatus, error: normalizedError };
|
|
1756
|
+
const authorityUpdate = this.db
|
|
1757
|
+
.prepare(
|
|
1758
|
+
`UPDATE scheduler_authority_reservations
|
|
1759
|
+
SET status = ?, outcome_json = ?, settled_at = ?
|
|
1760
|
+
WHERE occurrence_id = ? AND status = 'reserved'`,
|
|
1761
|
+
)
|
|
1762
|
+
.run(
|
|
1763
|
+
authorityOutcome,
|
|
1764
|
+
canonicalJson(authorityResult, "authoritySettlement"),
|
|
1765
|
+
now,
|
|
1766
|
+
id,
|
|
1767
|
+
);
|
|
1768
|
+
if (authorityUpdate.changes !== 1) {
|
|
1769
|
+
throw new SchedulerKernelError(
|
|
1770
|
+
"SCHEDULER_AUTHORITY_SETTLEMENT_CONFLICT",
|
|
1771
|
+
`Scheduler authority reservation could not be settled: ${id}`,
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1356
1775
|
this._appendEvent({
|
|
1357
1776
|
jobId: current.job_id,
|
|
1358
1777
|
occurrenceId: id,
|
|
@@ -1363,6 +1782,12 @@ export class SchedulerStore {
|
|
|
1363
1782
|
data: {
|
|
1364
1783
|
attempt: current.attempt,
|
|
1365
1784
|
maxAttempts: current.max_attempts,
|
|
1785
|
+
...(reservation
|
|
1786
|
+
? {
|
|
1787
|
+
authorityPolicyRevision: reservation.policy_revision,
|
|
1788
|
+
authorityUnits: reservation.units,
|
|
1789
|
+
}
|
|
1790
|
+
: {}),
|
|
1366
1791
|
...(nextStatus === OCCURRENCE_STATUS.RETRY_WAIT
|
|
1367
1792
|
? { retryAt: nextAvailableAt }
|
|
1368
1793
|
: {}),
|
|
@@ -1418,6 +1843,313 @@ export class SchedulerStore {
|
|
|
1418
1843
|
.map(mapOccurrence);
|
|
1419
1844
|
}
|
|
1420
1845
|
|
|
1846
|
+
setAuthorityPolicy(
|
|
1847
|
+
principal,
|
|
1848
|
+
{
|
|
1849
|
+
capabilities,
|
|
1850
|
+
windowMs,
|
|
1851
|
+
maxRuns,
|
|
1852
|
+
maxUnits,
|
|
1853
|
+
enabled = true,
|
|
1854
|
+
expectedRevision,
|
|
1855
|
+
} = {},
|
|
1856
|
+
) {
|
|
1857
|
+
const actor = normalizeAuthorityPrincipal(principal);
|
|
1858
|
+
const allowedCapabilities = normalizeCapabilityList(capabilities);
|
|
1859
|
+
const window = normalizeAuthorityBudgetLimit(windowMs, "windowMs", {
|
|
1860
|
+
minimum: MIN_AUTHORITY_WINDOW_MS,
|
|
1861
|
+
maximum: MAX_AUTHORITY_WINDOW_MS,
|
|
1862
|
+
});
|
|
1863
|
+
const runs = normalizeAuthorityBudgetLimit(maxRuns, "maxRuns");
|
|
1864
|
+
const units = normalizeAuthorityBudgetLimit(maxUnits, "maxUnits");
|
|
1865
|
+
const active = normalizeBoolean(enabled, "enabled");
|
|
1866
|
+
if (
|
|
1867
|
+
expectedRevision !== undefined &&
|
|
1868
|
+
(!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
1869
|
+
) {
|
|
1870
|
+
throw invalidArgument("expectedRevision must be an integer >= 0");
|
|
1871
|
+
}
|
|
1872
|
+
const now = this._now();
|
|
1873
|
+
return this._write(() => {
|
|
1874
|
+
const current = this.db
|
|
1875
|
+
.prepare(
|
|
1876
|
+
`SELECT * FROM scheduler_authority_policies
|
|
1877
|
+
WHERE principal_type = ? AND principal_id = ?`,
|
|
1878
|
+
)
|
|
1879
|
+
.get(actor.type, actor.id);
|
|
1880
|
+
const currentRevision = current?.revision ?? 0;
|
|
1881
|
+
if (
|
|
1882
|
+
expectedRevision !== undefined &&
|
|
1883
|
+
expectedRevision !== currentRevision
|
|
1884
|
+
) {
|
|
1885
|
+
throw new SchedulerKernelError(
|
|
1886
|
+
"SCHEDULER_AUTHORITY_POLICY_CONFLICT",
|
|
1887
|
+
"Scheduler authority policy revision does not match",
|
|
1888
|
+
{ expectedRevision, actualRevision: currentRevision },
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
const revision = currentRevision + 1;
|
|
1892
|
+
this.db
|
|
1893
|
+
.prepare(
|
|
1894
|
+
`INSERT INTO scheduler_authority_policies
|
|
1895
|
+
(principal_type, principal_id, revision, enabled,
|
|
1896
|
+
capabilities_json, window_ms, max_runs, max_units,
|
|
1897
|
+
created_at, updated_at)
|
|
1898
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1899
|
+
ON CONFLICT(principal_type, principal_id) DO UPDATE SET
|
|
1900
|
+
revision = excluded.revision,
|
|
1901
|
+
enabled = excluded.enabled,
|
|
1902
|
+
capabilities_json = excluded.capabilities_json,
|
|
1903
|
+
window_ms = excluded.window_ms,
|
|
1904
|
+
max_runs = excluded.max_runs,
|
|
1905
|
+
max_units = excluded.max_units,
|
|
1906
|
+
updated_at = excluded.updated_at`,
|
|
1907
|
+
)
|
|
1908
|
+
.run(
|
|
1909
|
+
actor.type,
|
|
1910
|
+
actor.id,
|
|
1911
|
+
revision,
|
|
1912
|
+
active ? 1 : 0,
|
|
1913
|
+
canonicalJson(allowedCapabilities, "authorityPolicy.capabilities"),
|
|
1914
|
+
window,
|
|
1915
|
+
runs,
|
|
1916
|
+
units,
|
|
1917
|
+
current?.created_at ?? now,
|
|
1918
|
+
now,
|
|
1919
|
+
);
|
|
1920
|
+
return this.getAuthorityPolicy(actor);
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
getAuthorityPolicy(principal) {
|
|
1925
|
+
this._assertOpen();
|
|
1926
|
+
const actor = normalizeAuthorityPrincipal(principal);
|
|
1927
|
+
return mapAuthorityPolicy(
|
|
1928
|
+
this.db
|
|
1929
|
+
.prepare(
|
|
1930
|
+
`SELECT * FROM scheduler_authority_policies
|
|
1931
|
+
WHERE principal_type = ? AND principal_id = ?`,
|
|
1932
|
+
)
|
|
1933
|
+
.get(actor.type, actor.id),
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
ensureAuthorityPolicy(
|
|
1938
|
+
authority,
|
|
1939
|
+
{
|
|
1940
|
+
windowMs = DEFAULT_AUTHORITY_WINDOW_MS,
|
|
1941
|
+
maxRuns = DEFAULT_AUTHORITY_MAX_RUNS,
|
|
1942
|
+
maxUnits = DEFAULT_AUTHORITY_MAX_UNITS,
|
|
1943
|
+
} = {},
|
|
1944
|
+
) {
|
|
1945
|
+
const normalized = normalizeAuthorityEnvelope(authority);
|
|
1946
|
+
const current = this.getAuthorityPolicy(normalized.principal);
|
|
1947
|
+
if (current) return current;
|
|
1948
|
+
try {
|
|
1949
|
+
return this.setAuthorityPolicy(normalized.principal, {
|
|
1950
|
+
capabilities: normalized.requestedCapabilities,
|
|
1951
|
+
windowMs,
|
|
1952
|
+
maxRuns,
|
|
1953
|
+
maxUnits,
|
|
1954
|
+
expectedRevision: 0,
|
|
1955
|
+
});
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
if (error?.code !== "SCHEDULER_AUTHORITY_POLICY_CONFLICT") throw error;
|
|
1958
|
+
const raced = this.getAuthorityPolicy(normalized.principal);
|
|
1959
|
+
if (raced) return raced;
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
getAuthorityReservation(occurrenceId) {
|
|
1965
|
+
this._assertOpen();
|
|
1966
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
1967
|
+
return mapAuthorityReservation(
|
|
1968
|
+
this.db
|
|
1969
|
+
.prepare(
|
|
1970
|
+
`SELECT * FROM scheduler_authority_reservations
|
|
1971
|
+
WHERE occurrence_id = ?`,
|
|
1972
|
+
)
|
|
1973
|
+
.get(id),
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
reserveAuthority({ occurrenceId, policyRevision, units = 1 } = {}) {
|
|
1978
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
1979
|
+
const expectedPolicyRevision = normalizeAuthorityBudgetLimit(
|
|
1980
|
+
policyRevision,
|
|
1981
|
+
"policyRevision",
|
|
1982
|
+
{ maximum: Number.MAX_SAFE_INTEGER },
|
|
1983
|
+
);
|
|
1984
|
+
const requestedUnits = normalizeAuthorityBudgetLimit(units, "units");
|
|
1985
|
+
const now = this._now();
|
|
1986
|
+
return this._write(() => {
|
|
1987
|
+
const occurrence = mapOccurrence(this.statements.getOccurrence.get(id));
|
|
1988
|
+
if (!occurrence) {
|
|
1989
|
+
throw new SchedulerKernelError(
|
|
1990
|
+
"SCHEDULER_NOT_FOUND",
|
|
1991
|
+
`Scheduler occurrence does not exist: ${id}`,
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (occurrence.status !== OCCURRENCE_STATUS.RUNNING) {
|
|
1995
|
+
throw new SchedulerKernelError(
|
|
1996
|
+
"SCHEDULER_AUTHORITY_OCCURRENCE_NOT_RUNNING",
|
|
1997
|
+
`Scheduler authority may only reserve a running occurrence: ${id}`,
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
const existing = this.db
|
|
2001
|
+
.prepare(
|
|
2002
|
+
`SELECT * FROM scheduler_authority_reservations
|
|
2003
|
+
WHERE occurrence_id = ?`,
|
|
2004
|
+
)
|
|
2005
|
+
.get(id);
|
|
2006
|
+
if (existing) {
|
|
2007
|
+
const reservation = mapAuthorityReservation(existing);
|
|
2008
|
+
if (
|
|
2009
|
+
reservation.jobId !== occurrence.jobId ||
|
|
2010
|
+
reservation.principal.type !== occurrence.authority.principal.type ||
|
|
2011
|
+
reservation.principal.id !== occurrence.authority.principal.id ||
|
|
2012
|
+
reservation.policyRevision !== expectedPolicyRevision ||
|
|
2013
|
+
reservation.units !== requestedUnits
|
|
2014
|
+
) {
|
|
2015
|
+
throw new SchedulerKernelError(
|
|
2016
|
+
"SCHEDULER_AUTHORITY_RESERVATION_MISMATCH",
|
|
2017
|
+
`Scheduler authority reservation identity is mismatched: ${id}`,
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
return { ...reservation, deduplicated: true };
|
|
2021
|
+
}
|
|
2022
|
+
const principal = occurrence.authority.principal;
|
|
2023
|
+
const policyRow = this.db
|
|
2024
|
+
.prepare(
|
|
2025
|
+
`SELECT * FROM scheduler_authority_policies
|
|
2026
|
+
WHERE principal_type = ? AND principal_id = ?`,
|
|
2027
|
+
)
|
|
2028
|
+
.get(principal.type, principal.id);
|
|
2029
|
+
const policy = mapAuthorityPolicy(policyRow);
|
|
2030
|
+
if (!policy || !policy.enabled) {
|
|
2031
|
+
throw new SchedulerKernelError(
|
|
2032
|
+
"SCHEDULER_AUTHORITY_POLICY_REQUIRED",
|
|
2033
|
+
`Scheduler authority policy is missing or disabled: ${principal.type}:${principal.id}`,
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
if (policy.revision !== expectedPolicyRevision) {
|
|
2037
|
+
throw new SchedulerKernelError(
|
|
2038
|
+
"SCHEDULER_AUTHORITY_POLICY_STALE",
|
|
2039
|
+
`Scheduler authority policy changed before reservation: ${principal.type}:${principal.id}`,
|
|
2040
|
+
{
|
|
2041
|
+
expectedRevision: expectedPolicyRevision,
|
|
2042
|
+
actualRevision: policy.revision,
|
|
2043
|
+
},
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const denied = occurrence.authority.requestedCapabilities.filter(
|
|
2047
|
+
(capability) =>
|
|
2048
|
+
!policy.capabilities.includes("*") &&
|
|
2049
|
+
!policy.capabilities.includes(capability),
|
|
2050
|
+
);
|
|
2051
|
+
if (denied.length > 0) {
|
|
2052
|
+
throw new SchedulerKernelError(
|
|
2053
|
+
"SCHEDULER_AUTHORITY_PERMISSION_DENIED",
|
|
2054
|
+
`Scheduler authority capability was denied: ${principal.type}:${principal.id}`,
|
|
2055
|
+
{ denied },
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
const windowStartedAt =
|
|
2059
|
+
Math.floor(now / policy.windowMs) * policy.windowMs;
|
|
2060
|
+
const usage = this.db
|
|
2061
|
+
.prepare(
|
|
2062
|
+
`SELECT runs, units FROM scheduler_authority_usage
|
|
2063
|
+
WHERE principal_type = ? AND principal_id = ?
|
|
2064
|
+
AND policy_revision = ? AND window_started_at = ?`,
|
|
2065
|
+
)
|
|
2066
|
+
.get(
|
|
2067
|
+
principal.type,
|
|
2068
|
+
principal.id,
|
|
2069
|
+
policy.revision,
|
|
2070
|
+
windowStartedAt,
|
|
2071
|
+
) ?? { runs: 0, units: 0 };
|
|
2072
|
+
const reservations = this.db
|
|
2073
|
+
.prepare(
|
|
2074
|
+
`SELECT COUNT(*) AS runs, COALESCE(SUM(units), 0) AS units
|
|
2075
|
+
FROM scheduler_authority_reservations
|
|
2076
|
+
WHERE principal_type = ? AND principal_id = ?
|
|
2077
|
+
AND policy_revision = ? AND window_started_at = ?`,
|
|
2078
|
+
)
|
|
2079
|
+
.get(principal.type, principal.id, policy.revision, windowStartedAt);
|
|
2080
|
+
if (
|
|
2081
|
+
reservations.runs !== usage.runs ||
|
|
2082
|
+
reservations.units !== usage.units
|
|
2083
|
+
) {
|
|
2084
|
+
throw new SchedulerKernelError(
|
|
2085
|
+
"SCHEDULER_AUTHORITY_BUDGET_STATE_INVALID",
|
|
2086
|
+
`Scheduler authority usage does not match reservations: ${principal.type}:${principal.id}`,
|
|
2087
|
+
{ usage, reservations },
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
const nextRuns = usage.runs + 1;
|
|
2091
|
+
const nextUnits = usage.units + requestedUnits;
|
|
2092
|
+
if (nextRuns > policy.maxRuns || nextUnits > policy.maxUnits) {
|
|
2093
|
+
throw new SchedulerKernelError(
|
|
2094
|
+
"SCHEDULER_AUTHORITY_BUDGET_EXHAUSTED",
|
|
2095
|
+
`Scheduler authority budget is exhausted: ${principal.type}:${principal.id}`,
|
|
2096
|
+
{
|
|
2097
|
+
windowStartedAt,
|
|
2098
|
+
windowEndsAt: windowStartedAt + policy.windowMs,
|
|
2099
|
+
usedRuns: usage.runs,
|
|
2100
|
+
usedUnits: usage.units,
|
|
2101
|
+
requestedUnits,
|
|
2102
|
+
maxRuns: policy.maxRuns,
|
|
2103
|
+
maxUnits: policy.maxUnits,
|
|
2104
|
+
},
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
this.db
|
|
2108
|
+
.prepare(
|
|
2109
|
+
`INSERT INTO scheduler_authority_usage
|
|
2110
|
+
(principal_type, principal_id, policy_revision,
|
|
2111
|
+
window_started_at, runs, units, updated_at)
|
|
2112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2113
|
+
ON CONFLICT(principal_type, principal_id, policy_revision,
|
|
2114
|
+
window_started_at) DO UPDATE SET
|
|
2115
|
+
runs = excluded.runs,
|
|
2116
|
+
units = excluded.units,
|
|
2117
|
+
updated_at = excluded.updated_at`,
|
|
2118
|
+
)
|
|
2119
|
+
.run(
|
|
2120
|
+
principal.type,
|
|
2121
|
+
principal.id,
|
|
2122
|
+
policy.revision,
|
|
2123
|
+
windowStartedAt,
|
|
2124
|
+
nextRuns,
|
|
2125
|
+
nextUnits,
|
|
2126
|
+
now,
|
|
2127
|
+
);
|
|
2128
|
+
this.db
|
|
2129
|
+
.prepare(
|
|
2130
|
+
`INSERT INTO scheduler_authority_reservations
|
|
2131
|
+
(occurrence_id, job_id, principal_type, principal_id,
|
|
2132
|
+
policy_revision, window_started_at, units, status,
|
|
2133
|
+
outcome_json, created_at, settled_at)
|
|
2134
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'reserved', NULL, ?, NULL)`,
|
|
2135
|
+
)
|
|
2136
|
+
.run(
|
|
2137
|
+
id,
|
|
2138
|
+
occurrence.jobId,
|
|
2139
|
+
principal.type,
|
|
2140
|
+
principal.id,
|
|
2141
|
+
policy.revision,
|
|
2142
|
+
windowStartedAt,
|
|
2143
|
+
requestedUnits,
|
|
2144
|
+
now,
|
|
2145
|
+
);
|
|
2146
|
+
return {
|
|
2147
|
+
...this.getAuthorityReservation(id),
|
|
2148
|
+
deduplicated: false,
|
|
2149
|
+
};
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1421
2153
|
schemaInfo() {
|
|
1422
2154
|
this._assertOpen();
|
|
1423
2155
|
return {
|