@apisurf/ngn-core 0.3.1 → 0.5.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/core.d.ts +465 -66
- package/dist/core.js +1642 -170
- package/package.json +4 -4
package/dist/core.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { RunStage } from '@apisurf/ngn-schema/describe';
|
|
2
|
-
export { RunStage } from '@apisurf/ngn-schema/describe';
|
|
3
|
-
import { FsNodeFileDescriptor } from '@apisurf/ngn-os';
|
|
4
1
|
import { Client, InArgs, ResultSet, InStatement, InValue } from '@apisurf/ngn-persistence';
|
|
5
2
|
export { initDbFileIfNotExists } from '@apisurf/ngn-persistence';
|
|
6
|
-
import {
|
|
3
|
+
import { ScheduleFields, ScheduleEventName, TickOrigin, RunStage } from '@apisurf/ngn-schema/describe';
|
|
4
|
+
export { RunStage } from '@apisurf/ngn-schema/describe';
|
|
5
|
+
import { FsNodeFileDescriptor } from '@apisurf/ngn-os';
|
|
7
6
|
|
|
8
7
|
type CompilerOptions = {
|
|
9
8
|
/** Minify output */
|
|
@@ -59,6 +58,95 @@ declare class Compiler {
|
|
|
59
58
|
*/
|
|
60
59
|
declare function executeEsm(code: string): Promise<Record<string, unknown>>;
|
|
61
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Named leases in `locks`.
|
|
63
|
+
*
|
|
64
|
+
* Taking one is a single upsert that only overwrites a row whose lease has
|
|
65
|
+
* run out, so two processes on one file cannot both win, and a lease left by
|
|
66
|
+
* a process that died is free once it expires. Every acquisition gets its own
|
|
67
|
+
* holder token: two runs of one task in one process are two holders.
|
|
68
|
+
*/
|
|
69
|
+
declare class LockService {
|
|
70
|
+
private db;
|
|
71
|
+
constructor(db: Client);
|
|
72
|
+
/** The holder token on success, null when someone else holds an unexpired lease. */
|
|
73
|
+
tryAcquire(name: string, ttlMs: number, taskRunId: number | null): Promise<string | null>;
|
|
74
|
+
/** Push the lease out; false when it was lost (it expired and someone took it). */
|
|
75
|
+
renew(name: string, holder: string, ttlMs: number): Promise<boolean>;
|
|
76
|
+
/** Who holds `name`, and until when; null when nobody has it. Expired leases included. */
|
|
77
|
+
holderOf(name: string): Promise<{
|
|
78
|
+
holder: string;
|
|
79
|
+
taskRunId: number | null;
|
|
80
|
+
expiresAt: number;
|
|
81
|
+
} | null>;
|
|
82
|
+
/** Record which run a held lease is working for. */
|
|
83
|
+
setRun(name: string, holder: string, taskRunId: number): Promise<void>;
|
|
84
|
+
release(name: string, holder: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A length of time as tasks write it: milliseconds, or a string such as
|
|
89
|
+
* `"500ms"`, `"30s"`, `"4m"`, `"2h"` or `"1d"`.
|
|
90
|
+
*/
|
|
91
|
+
type Duration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
|
|
92
|
+
|
|
93
|
+
type Edge = "leading" | "trailing";
|
|
94
|
+
/** The object form of the `timing` export: a period instead of a cron pattern. */
|
|
95
|
+
interface IntervalTiming {
|
|
96
|
+
/** How far apart runs are. At least 1s. */
|
|
97
|
+
every: Duration;
|
|
98
|
+
/**
|
|
99
|
+
* `"leading"` (default): a fixed grid, `every` apart from the planned start
|
|
100
|
+
* of the last run. `"trailing"`: `every` after the last run ended.
|
|
101
|
+
*/
|
|
102
|
+
edge?: Edge;
|
|
103
|
+
/** The shortest delay a run may choose. At least 1s; defaults to `every`. */
|
|
104
|
+
min?: Duration;
|
|
105
|
+
/** The longest delay anything may choose. Defaults to `every × 32`. */
|
|
106
|
+
max?: Duration;
|
|
107
|
+
/**
|
|
108
|
+
* How failures slow the task down: `every × factor^failedInARow`, capped at
|
|
109
|
+
* `max`, give or take `jitter` of it. `false` keeps failures on `every`.
|
|
110
|
+
* Defaults to `{ factor: 2, jitter: 0.1 }`.
|
|
111
|
+
*/
|
|
112
|
+
backoff?: TimingBackoff | false;
|
|
113
|
+
}
|
|
114
|
+
interface TimingBackoff {
|
|
115
|
+
/** What each failure in a row multiplies the delay by. At least 1. */
|
|
116
|
+
factor?: number;
|
|
117
|
+
/** How far the delay may stray either way, as a fraction of it, from 0 to 1. */
|
|
118
|
+
jitter?: number;
|
|
119
|
+
}
|
|
120
|
+
/** What a task's `timing` export can be. */
|
|
121
|
+
type TimingExport = string | IntervalTiming;
|
|
122
|
+
type ResolvedTiming = {
|
|
123
|
+
kind: "cron";
|
|
124
|
+
pattern: string;
|
|
125
|
+
} | {
|
|
126
|
+
kind: "interval";
|
|
127
|
+
everyMs: number;
|
|
128
|
+
edge: Edge;
|
|
129
|
+
minMs: number;
|
|
130
|
+
maxMs: number;
|
|
131
|
+
backoff: Required<TimingBackoff> | null;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* The `timing` export, checked: a cron pattern, or `{ every, edge, min, max, backoff }`. Throws,
|
|
135
|
+
* naming the task, on anything else, so a typo fails at schedule time.
|
|
136
|
+
*/
|
|
137
|
+
declare function resolveTiming(value: unknown, taskPath: string): ResolvedTiming;
|
|
138
|
+
/**
|
|
139
|
+
* The timing `--timing` takes on the command line: a duration (`5m`) is
|
|
140
|
+
* `{ every }` on the leading edge, anything else a cron pattern. Null when it is
|
|
141
|
+
* neither.
|
|
142
|
+
*/
|
|
143
|
+
declare function timingFromFlag(flag: string): TimingExport | null;
|
|
144
|
+
/** What the `next` hook chose, checked: a delay in ms or one of the task's own, and why. */
|
|
145
|
+
interface NextChoice {
|
|
146
|
+
delay: number | "every" | "min" | "max";
|
|
147
|
+
reason: string | null;
|
|
148
|
+
}
|
|
149
|
+
|
|
62
150
|
/**
|
|
63
151
|
* A database migration
|
|
64
152
|
*/
|
|
@@ -130,30 +218,6 @@ declare function closeTaskDatabases(): void;
|
|
|
130
218
|
*/
|
|
131
219
|
declare function releaseTaskDatabases(taskSourcePath: string): void;
|
|
132
220
|
|
|
133
|
-
/**
|
|
134
|
-
* A length of time as tasks write it: milliseconds, or a string such as
|
|
135
|
-
* `"500ms"`, `"30s"`, `"4m"`, `"2h"` or `"1d"`.
|
|
136
|
-
*/
|
|
137
|
-
type Duration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Named leases in `locks`.
|
|
141
|
-
*
|
|
142
|
-
* Taking one is a single upsert that only overwrites a row whose lease has
|
|
143
|
-
* run out, so two processes on one file cannot both win, and a lease left by
|
|
144
|
-
* a process that died is free once it expires. Every acquisition gets its own
|
|
145
|
-
* holder token: two runs of one task in one process are two holders.
|
|
146
|
-
*/
|
|
147
|
-
declare class LockService {
|
|
148
|
-
private db;
|
|
149
|
-
constructor(db: Client);
|
|
150
|
-
/** The holder token on success, null when someone else holds an unexpired lease. */
|
|
151
|
-
tryAcquire(name: string, ttlMs: number, taskRunId: number | null): Promise<string | null>;
|
|
152
|
-
/** Push the lease out; false when it was lost (it expired and someone took it). */
|
|
153
|
-
renew(name: string, holder: string, ttlMs: number): Promise<boolean>;
|
|
154
|
-
release(name: string, holder: string): Promise<void>;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
221
|
interface LockOptions {
|
|
158
222
|
/**
|
|
159
223
|
* How long the lease lasts if this process stops renewing it (it dies, or
|
|
@@ -451,6 +515,15 @@ declare class Queue<T = unknown> {
|
|
|
451
515
|
stats(): Promise<QueueStats>;
|
|
452
516
|
}
|
|
453
517
|
|
|
518
|
+
interface ResolvedRetry {
|
|
519
|
+
attempts: number;
|
|
520
|
+
delayMs: number;
|
|
521
|
+
breaker: {
|
|
522
|
+
after: number;
|
|
523
|
+
maxSkip: number;
|
|
524
|
+
} | null;
|
|
525
|
+
}
|
|
526
|
+
|
|
454
527
|
/**
|
|
455
528
|
* The run this context belongs to, and how long it has. Times are epoch
|
|
456
529
|
* milliseconds, as everywhere in ngn's database.
|
|
@@ -458,9 +531,13 @@ declare class Queue<T = unknown> {
|
|
|
458
531
|
interface TaskRun {
|
|
459
532
|
/** This run's `task_runs.id`. */
|
|
460
533
|
id: number;
|
|
461
|
-
/** When the tick that started it
|
|
534
|
+
/** When the tick that started it was planned for; null for a run with no schedule. */
|
|
462
535
|
scheduledAt: number | null;
|
|
463
|
-
/**
|
|
536
|
+
/**
|
|
537
|
+
* When the next tick is due: the next slot of a cron pattern or the leading
|
|
538
|
+
* edge. Null on the trailing edge, whose next run depends on when this one
|
|
539
|
+
* ends, and for a run with no schedule.
|
|
540
|
+
*/
|
|
464
541
|
nextAt: number | null;
|
|
465
542
|
/**
|
|
466
543
|
* When to stop starting new work: shortly before `nextAt`, or sooner with a
|
|
@@ -527,11 +604,118 @@ interface TaskContext {
|
|
|
527
604
|
sqlite: TaskSqlite;
|
|
528
605
|
}
|
|
529
606
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
607
|
+
* ngn runs one run of a task at a time, so `"skip"` is the only policy: a run
|
|
608
|
+
* that comes due while one is going waits for it. `"allow"` fails at schedule
|
|
609
|
+
* time; it is kept in the type so the message can say why.
|
|
533
610
|
*/
|
|
534
611
|
type OverlapPolicy = "skip" | "allow";
|
|
612
|
+
interface NextOutcomeBase {
|
|
613
|
+
/** Failed ticks in a row, this one included. */
|
|
614
|
+
failedInARow: number;
|
|
615
|
+
/** The delay that led to this tick, in ms; null when there was none to measure. */
|
|
616
|
+
previousDelayMs: number | null;
|
|
617
|
+
/** The `task_runs.id` of the tick's final run. */
|
|
618
|
+
runId: number;
|
|
619
|
+
}
|
|
620
|
+
/** What the `next` hook is told about the tick that just ended. */
|
|
621
|
+
type NextOutcome<Result = any> = NextOutcomeBase & ({
|
|
622
|
+
status: "success";
|
|
623
|
+
result: Result;
|
|
624
|
+
error: undefined;
|
|
625
|
+
skipReason: null;
|
|
626
|
+
} | {
|
|
627
|
+
status: "failure";
|
|
628
|
+
result: undefined;
|
|
629
|
+
error: Error;
|
|
630
|
+
skipReason: null;
|
|
631
|
+
} | {
|
|
632
|
+
status: "skipped";
|
|
633
|
+
result: undefined;
|
|
634
|
+
error: undefined;
|
|
635
|
+
/** Why ngn skipped it; null for a skip by the task's own `shouldSkip`. */
|
|
636
|
+
skipReason: string | null;
|
|
637
|
+
});
|
|
638
|
+
/**
|
|
639
|
+
* When to run next: a duration from when the tick ended, or the task's own
|
|
640
|
+
* `every`, `min` or `max`. ngn keeps it within `[min, max]`.
|
|
641
|
+
*/
|
|
642
|
+
type NextDelay = Duration | "every" | "min" | "max";
|
|
643
|
+
/** What the `next` hook returns: undefined leaves it to the default policy. */
|
|
644
|
+
type NextDecision = undefined | void | NextDelay | {
|
|
645
|
+
in: NextDelay;
|
|
646
|
+
reason?: string;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
interface ScheduleRow extends ScheduleFields {
|
|
650
|
+
file_task_id: number;
|
|
651
|
+
updated_at: number;
|
|
652
|
+
}
|
|
653
|
+
/** What a transition writes, besides the state the table gives it. */
|
|
654
|
+
interface Transition {
|
|
655
|
+
event: ScheduleEventName;
|
|
656
|
+
at: number;
|
|
657
|
+
/** Row fields to set; anything left out keeps its value. */
|
|
658
|
+
set?: Partial<Omit<ScheduleFields, "state">>;
|
|
659
|
+
origin?: TickOrigin | null;
|
|
660
|
+
firstRunId?: number | null;
|
|
661
|
+
runId?: number | null;
|
|
662
|
+
/** Only apply while the row's holder is this one (a tick's own lease, or none). */
|
|
663
|
+
holder?: string | null;
|
|
664
|
+
/** The tick's final run, to record the next run and its reason on. */
|
|
665
|
+
finalRunId?: number | null;
|
|
666
|
+
/**
|
|
667
|
+
* Only apply while the row's pending request is this one, so a request made
|
|
668
|
+
* after it was read is not lost.
|
|
669
|
+
*/
|
|
670
|
+
requestIs?: string | null;
|
|
671
|
+
/**
|
|
672
|
+
* A second transition to make in the same write, from the state this one
|
|
673
|
+
* leaves: `paused` after the decision of a tick a pause was requested during.
|
|
674
|
+
*/
|
|
675
|
+
followedBy?: Omit<Transition, "followedBy" | "holder" | "requestIs" | "finalRunId">;
|
|
676
|
+
}
|
|
677
|
+
/** Why a transition was not applied. */
|
|
678
|
+
type Refusal = {
|
|
679
|
+
refused: string;
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* The schedule rows and their event log. Every change is one write batch: the
|
|
683
|
+
* row update, conditional on the state the event comes from, then the event,
|
|
684
|
+
* copied from the row it left, inserted only if the update applied. The row is
|
|
685
|
+
* therefore always the fold of its events. A write the state table does not
|
|
686
|
+
* allow, or one that lost a race with another process, is refused and the row
|
|
687
|
+
* is left as it was.
|
|
688
|
+
*/
|
|
689
|
+
declare class ScheduleStore {
|
|
690
|
+
private db;
|
|
691
|
+
constructor(db: Client);
|
|
692
|
+
read(taskId: number): Promise<ScheduleRow | null>;
|
|
693
|
+
/**
|
|
694
|
+
* Apply `change` to the task's schedule, as it stands now. The new row, or
|
|
695
|
+
* why it was refused.
|
|
696
|
+
*/
|
|
697
|
+
transition(taskId: number, change: Transition): Promise<ScheduleRow | Refusal>;
|
|
698
|
+
private insert;
|
|
699
|
+
/**
|
|
700
|
+
* The row update, conditional on the state it comes from (and the holder or
|
|
701
|
+
* request the change names). `after`: the time of the transition this one
|
|
702
|
+
* follows in the same write, which it applies only on top of.
|
|
703
|
+
*/
|
|
704
|
+
private update;
|
|
705
|
+
/** The next run and its reason, on the tick's final run, written only if the event was. */
|
|
706
|
+
private finalRun;
|
|
707
|
+
/** The event, copied from the row the update left, written only if it applied. */
|
|
708
|
+
private event;
|
|
709
|
+
/**
|
|
710
|
+
* Close the task's runs that never recorded an end, as failed in
|
|
711
|
+
* `interrupted` with `why` as the error, in one write. `fromRunId` limits it
|
|
712
|
+
* to one tick's runs. Returns the ids closed, oldest first.
|
|
713
|
+
*/
|
|
714
|
+
closeOpenRuns(taskId: number, why: string, { fromRunId, at }: {
|
|
715
|
+
fromRunId?: number;
|
|
716
|
+
at: number;
|
|
717
|
+
}): Promise<number[]>;
|
|
718
|
+
}
|
|
535
719
|
|
|
536
720
|
interface CompiledTaskCallbacks {
|
|
537
721
|
onTaskLoadComplete?: ({ paths, md5Hash, compiledCode, }: {
|
|
@@ -627,13 +811,74 @@ type TaskConfig = {
|
|
|
627
811
|
env: TaskContext["env"];
|
|
628
812
|
kv: TaskContext["kv"];
|
|
629
813
|
sqlite: TaskContext["sqlite"];
|
|
814
|
+
/** The run database's schedules and leases, for whatever schedules or starts the task. */
|
|
815
|
+
scheduling: {
|
|
816
|
+
store: ScheduleStore;
|
|
817
|
+
locks: LockService;
|
|
818
|
+
};
|
|
630
819
|
};
|
|
631
820
|
|
|
821
|
+
/**
|
|
822
|
+
* What time it is, and how to wait: the one seam the scheduler takes time
|
|
823
|
+
* through, so a test can drive grids, sleeps and restarts without patching
|
|
824
|
+
* global timers (which task code, in its own VM context, never sees anyway).
|
|
825
|
+
*/
|
|
826
|
+
interface Clock {
|
|
827
|
+
now(): number;
|
|
828
|
+
/** Resolve after `ms`, or as soon as `signal` aborts. Never rejects. */
|
|
829
|
+
sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
830
|
+
}
|
|
831
|
+
declare const systemClock: Clock;
|
|
832
|
+
|
|
632
833
|
interface ExecuteSchedule {
|
|
633
834
|
/** When the tick fired, epoch ms. */
|
|
634
835
|
scheduledAt?: number | null;
|
|
635
836
|
/** When the next tick is due, epoch ms. */
|
|
636
837
|
nextAt?: number | null;
|
|
838
|
+
/**
|
|
839
|
+
* For a run with no next tick: its deadline when it has no budget, measured
|
|
840
|
+
* from its start (the trailing edge's `every`).
|
|
841
|
+
*/
|
|
842
|
+
periodMs?: number | null;
|
|
843
|
+
/** Where the tick reads the time and waits between retries. */
|
|
844
|
+
clock?: Clock;
|
|
845
|
+
/** Aborts every attempt's `ctx.run.signal`: ngn is stopping, or lost the task's lease. */
|
|
846
|
+
signal?: AbortSignal;
|
|
847
|
+
/** Told each attempt's run id as its row is opened. */
|
|
848
|
+
onRunStarted?: (runId: number) => void;
|
|
849
|
+
/**
|
|
850
|
+
* Ask the task's `next` hook, if it has one, when to run next, once the
|
|
851
|
+
* tick has ended: a scheduled tick of an interval task passes this.
|
|
852
|
+
*/
|
|
853
|
+
next?: {
|
|
854
|
+
previousDelayMs: number | null;
|
|
855
|
+
timeoutMs?: number;
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
type TickStatus = "success" | "failure" | "skipped";
|
|
859
|
+
/**
|
|
860
|
+
* How one tick ended, every way it can: the task ran and returned, it failed
|
|
861
|
+
* (in any stage, after any retries), or ngn or `shouldSkip` held it back.
|
|
862
|
+
* Whatever decides when the task runs next reads this.
|
|
863
|
+
*/
|
|
864
|
+
interface TickOutcome {
|
|
865
|
+
status: TickStatus;
|
|
866
|
+
/** What the task returned, for a success. */
|
|
867
|
+
result: unknown;
|
|
868
|
+
/** What the final attempt threw, for a failure. */
|
|
869
|
+
error: unknown;
|
|
870
|
+
/** Why ngn skipped the tick; null for a skip by the task's own `shouldSkip`, or no skip. */
|
|
871
|
+
skipReason: string | null;
|
|
872
|
+
/** The `task_runs.id` of the tick's first attempt; null when it failed before a run existed. */
|
|
873
|
+
firstRunId: number | null;
|
|
874
|
+
/** The `task_runs.id` of the tick's final attempt; null when it failed before a run existed. */
|
|
875
|
+
runId: number | null;
|
|
876
|
+
/** Attempts made, the first included. */
|
|
877
|
+
attempts: number;
|
|
878
|
+
/** When the final attempt ended, on the tick's clock. */
|
|
879
|
+
endedAt: number;
|
|
880
|
+
/** What the `next` hook chose; null when it was not asked, or left it to the policy. */
|
|
881
|
+
next: NextChoice | null;
|
|
637
882
|
}
|
|
638
883
|
/** The `task_runs.id` of the run that threw `error`, when a run recorded it. */
|
|
639
884
|
declare function failedRunId(error: unknown): number | null;
|
|
@@ -654,10 +899,9 @@ declare class Task {
|
|
|
654
899
|
private kv;
|
|
655
900
|
private sqlite;
|
|
656
901
|
private meta;
|
|
902
|
+
private schedulingServices;
|
|
657
903
|
private entry;
|
|
658
904
|
private entryExports;
|
|
659
|
-
/** Executions started and not yet settled. */
|
|
660
|
-
private inFlight;
|
|
661
905
|
constructor({ entryParams, buildConfigFn, }: {
|
|
662
906
|
entryParams: EntryParams;
|
|
663
907
|
buildConfigFn: BuildCompiledTaskConfigFn;
|
|
@@ -669,25 +913,28 @@ declare class Task {
|
|
|
669
913
|
*/
|
|
670
914
|
get taskPath(): string;
|
|
671
915
|
get codeHash(): string | null;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
*/
|
|
677
|
-
get overlapPolicy(): OverlapPolicy;
|
|
916
|
+
/** The `timing` export, as the latest run (or `loadEntry`) read it. */
|
|
917
|
+
get timing(): TimingExport | undefined;
|
|
918
|
+
/** The `next` export, as loaded: unchecked, for whoever schedules the task to check. */
|
|
919
|
+
get nextHook(): unknown;
|
|
678
920
|
/**
|
|
679
921
|
* Check the exports that shape how the task is scheduled, so a bad value
|
|
680
|
-
* fails when the task is scheduled rather than on its first tick.
|
|
922
|
+
* fails when the task is scheduled rather than on its first tick. ngn runs
|
|
923
|
+
* one run of a task at a time, so `overlap` may only say so.
|
|
681
924
|
*/
|
|
682
925
|
validateExports(): void;
|
|
683
|
-
/**
|
|
684
|
-
get
|
|
926
|
+
/** The `retry` export, checked. */
|
|
927
|
+
get retryPolicy(): ResolvedRetry;
|
|
928
|
+
/** The schedule and lease services this task's runs are recorded with. */
|
|
929
|
+
get scheduling(): {
|
|
930
|
+
store: ScheduleStore;
|
|
931
|
+
locks: LockService;
|
|
932
|
+
};
|
|
685
933
|
/**
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
* execution, which is always so for a run skipped because one is in progress.
|
|
934
|
+
* Load the task file and register it and its version, without running it:
|
|
935
|
+
* its schedule is kept against the task's id. Returns that id.
|
|
689
936
|
*/
|
|
690
|
-
|
|
937
|
+
register(): Promise<number>;
|
|
691
938
|
hasTaskExport(): boolean;
|
|
692
939
|
hasTimingExport(): boolean;
|
|
693
940
|
loadEntry(): Promise<void>;
|
|
@@ -712,18 +959,39 @@ declare class Task {
|
|
|
712
959
|
*/
|
|
713
960
|
private runHook;
|
|
714
961
|
/**
|
|
715
|
-
* Run the task for one tick, retrying as its `retry` export allows
|
|
716
|
-
* scheduled tick passes when it fired and when the next one
|
|
717
|
-
* `ctx.run.deadline` and the retry window are worked out from.
|
|
962
|
+
* Run the task for one tick, retrying as its `retry` export allows, and say
|
|
963
|
+
* how it ended. A scheduled tick passes when it fired and when the next one
|
|
964
|
+
* is due, which `ctx.run.deadline` and the retry window are worked out from.
|
|
965
|
+
* Never throws: a failure is an outcome, its error recorded on its run.
|
|
718
966
|
*/
|
|
719
|
-
|
|
967
|
+
executeTick(schedule?: ExecuteSchedule): Promise<TickOutcome>;
|
|
968
|
+
/**
|
|
969
|
+
* One tick, for a caller that wants the task's value: what it returned, or
|
|
970
|
+
* undefined for a skip. Throws what the tick failed with.
|
|
971
|
+
*/
|
|
972
|
+
execute(schedule?: ExecuteSchedule): Promise<unknown>;
|
|
720
973
|
private executeWithRetries;
|
|
974
|
+
/**
|
|
975
|
+
* Ask the `next` hook when to run next. What it throws, returns that it may
|
|
976
|
+
* not, or a hook that takes too long, is recorded on the tick's final run in
|
|
977
|
+
* stage `next`, and leaves the decision to the policy, as does a tick that
|
|
978
|
+
* never got as far as loading the task's exports.
|
|
979
|
+
*/
|
|
980
|
+
private askNext;
|
|
981
|
+
/** The tick's attempts, retrying as `retry` allows, and how the last one ended. */
|
|
982
|
+
private attempts;
|
|
721
983
|
/**
|
|
722
984
|
* Retry only a run that got as far as running: a module that will not load
|
|
723
985
|
* fails the same way every time. Then it has to fit before the next tick,
|
|
724
986
|
* and the task's `shouldRetry` gets the last word.
|
|
725
987
|
*/
|
|
726
988
|
private shouldRetry;
|
|
989
|
+
/**
|
|
990
|
+
* The task's latest ticks that failed in a row, from its run history, so a
|
|
991
|
+
* restart does not reset the count. Ticks closed as interrupted count, since
|
|
992
|
+
* their runs were closed as failed; skipped ticks neither break nor extend it.
|
|
993
|
+
*/
|
|
994
|
+
failedInARow(): Promise<number>;
|
|
727
995
|
/**
|
|
728
996
|
* Whether the breaker holds this tick back: after `after` failed ticks in a
|
|
729
997
|
* row it skips 1, 2, 4… ticks (counted since the latest failure), letting
|
|
@@ -765,20 +1033,132 @@ declare class TaskLibrary {
|
|
|
765
1033
|
tasksRootDir: string;
|
|
766
1034
|
buildConfigFn: BuildCompiledTaskConfigFn;
|
|
767
1035
|
}): void;
|
|
768
|
-
execute(taskPath: string): Promise<
|
|
1036
|
+
execute(taskPath: string): Promise<unknown>;
|
|
769
1037
|
getTasks(): Task[];
|
|
770
1038
|
}
|
|
771
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* What `ngn pause`, `ngn resume` and `ngn next` do to a task's schedule, and
|
|
1042
|
+
* what ngnui's buttons do through `ngn run`: each one transition from the
|
|
1043
|
+
* state table, written straight to the database with source `cli`. No live
|
|
1044
|
+
* runtime is needed; a running scheduler sees the changed row within a step.
|
|
1045
|
+
*
|
|
1046
|
+
* | Command | waiting, blocked | running | paused |
|
|
1047
|
+
* | -------- | ---------------- | ------------------ | ------------------- |
|
|
1048
|
+
* | pause | paused | pause_requested | no change |
|
|
1049
|
+
* | resume | no change | no change | resumed |
|
|
1050
|
+
* | next | rescheduled | override_requested | refused |
|
|
1051
|
+
*
|
|
1052
|
+
* A request made during a run waits in `schedules.request` for the run to end.
|
|
1053
|
+
*/
|
|
1054
|
+
|
|
1055
|
+
type ScheduleCommand = {
|
|
1056
|
+
kind: "pause";
|
|
1057
|
+
} | {
|
|
1058
|
+
kind: "resume";
|
|
1059
|
+
}
|
|
1060
|
+
/** `delayMs` from now; 0 for `now`. */
|
|
1061
|
+
| {
|
|
1062
|
+
kind: "next";
|
|
1063
|
+
delayMs: number;
|
|
1064
|
+
};
|
|
1065
|
+
interface ScheduleControlTarget {
|
|
1066
|
+
store: ScheduleStore;
|
|
1067
|
+
taskId: number;
|
|
1068
|
+
/** The task as the person named it, for the messages. */
|
|
1069
|
+
name: string;
|
|
1070
|
+
/** The task's cron pattern, when it runs on one: resuming it takes the next slot. */
|
|
1071
|
+
pattern: string | null;
|
|
1072
|
+
now: number;
|
|
1073
|
+
}
|
|
1074
|
+
type ScheduleControlResult = {
|
|
1075
|
+
applied: true;
|
|
1076
|
+
event: ScheduleEventName;
|
|
1077
|
+
row: ScheduleRow;
|
|
1078
|
+
message: string;
|
|
1079
|
+
}
|
|
1080
|
+
/** `error`: the command could not do what was asked (as opposed to there being nothing to do). */
|
|
1081
|
+
| {
|
|
1082
|
+
applied: false;
|
|
1083
|
+
error: boolean;
|
|
1084
|
+
message: string;
|
|
1085
|
+
};
|
|
1086
|
+
/**
|
|
1087
|
+
* Apply `command` to the task's schedule as it stands. If the scheduler moves
|
|
1088
|
+
* the row on between the read and the write, the command reads it again and
|
|
1089
|
+
* acts on the new state, once.
|
|
1090
|
+
*/
|
|
1091
|
+
declare function controlSchedule(target: ScheduleControlTarget, command: ScheduleCommand): Promise<ScheduleControlResult>;
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* When a task runs next, for each kind of timing. The scheduler's loop is the
|
|
1095
|
+
* same for all of them; only these answers differ.
|
|
1096
|
+
*
|
|
1097
|
+
* The leading edge and cron both run on slots: the leading edge's are `every`
|
|
1098
|
+
* apart from an anchor, a pattern's are wherever it matches. After a tick, the
|
|
1099
|
+
* next run is the first slot that fits: the slot after the tick's planned
|
|
1100
|
+
* time, or, when the run overran it, the first slot after it ended. Missed
|
|
1101
|
+
* slots are never made up with back-to-back runs.
|
|
1102
|
+
*/
|
|
1103
|
+
|
|
1104
|
+
/** The window a run gets: what `ctx.run` and the retry window are worked out from. */
|
|
1105
|
+
interface RunWindow {
|
|
1106
|
+
scheduledAt: number;
|
|
1107
|
+
nextAt: number | null;
|
|
1108
|
+
/** For the trailing edge: the run's deadline when it has no budget. */
|
|
1109
|
+
periodMs: number | null;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** What Run now did: started a run, or why it did not. */
|
|
1113
|
+
type RunNowOutcome = {
|
|
1114
|
+
started: true;
|
|
1115
|
+
} | {
|
|
1116
|
+
started: false;
|
|
1117
|
+
reason: string;
|
|
1118
|
+
};
|
|
1119
|
+
/** How long `stop()` waits for runs in progress before closing them as interrupted. */
|
|
1120
|
+
declare const STOP_GRACE_MS = 10000;
|
|
1121
|
+
/** The services a manual run uses, and the task's id they are keyed by. */
|
|
1122
|
+
interface ManualRunDeps {
|
|
1123
|
+
task: Task;
|
|
1124
|
+
taskId: number;
|
|
1125
|
+
clock: Clock;
|
|
1126
|
+
leaseTtlMs?: number;
|
|
1127
|
+
/** The run's window, when it has one: Run now's is the schedule's next run. */
|
|
1128
|
+
window?: Pick<RunWindow, "nextAt" | "periodMs">;
|
|
1129
|
+
onProblem: (message: string) => void;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* `ngn run x --once`: one run now, through the task's lease, from a process of
|
|
1133
|
+
* its own. Refused, writing nothing, while another run of the task is going.
|
|
1134
|
+
*/
|
|
1135
|
+
declare function runOnce(task: Task, { clock, leaseTtlMs, onProblem }: Pick<ManualRunDeps, "clock" | "leaseTtlMs" | "onProblem">): Promise<TickOutcome | {
|
|
1136
|
+
busy: string;
|
|
1137
|
+
}>;
|
|
1138
|
+
|
|
772
1139
|
type Options$1 = {
|
|
773
1140
|
taskLibrary: TaskLibrary;
|
|
774
1141
|
/** Called when a scheduled run throws, with the run it was recorded as when there is one. */
|
|
775
1142
|
onRunError?: (path: string, error: unknown, runId: number | null) => void;
|
|
1143
|
+
/** Something went wrong with a task's schedule itself (a refused transition, a bad timing change). */
|
|
1144
|
+
onScheduleProblem?: (path: string, message: string) => void;
|
|
1145
|
+
/** Schedule every task on this instead of its own `timing` (`ngn run x --timing`). */
|
|
1146
|
+
timing?: TimingExport;
|
|
1147
|
+
/** Where the scheduler reads the time and waits. Tests pass their own. */
|
|
1148
|
+
clock?: Clock;
|
|
1149
|
+
/** How long a task's lease outlives a process that stopped renewing it. */
|
|
1150
|
+
leaseTtlMs?: number;
|
|
1151
|
+
/** Where backoff's jitter comes from. Tests pass their own. */
|
|
1152
|
+
random?: () => number;
|
|
1153
|
+
/** How long a task's `next` hook may take. Tests pass their own. */
|
|
1154
|
+
nextTimeoutMs?: number;
|
|
776
1155
|
};
|
|
777
|
-
|
|
1156
|
+
|
|
1157
|
+
/** What `start()` did with each task: scheduled on its timing, or left out and why. */
|
|
778
1158
|
interface ScheduleReport {
|
|
779
1159
|
scheduled: {
|
|
780
1160
|
path: string;
|
|
781
|
-
|
|
1161
|
+
timing: TimingExport;
|
|
782
1162
|
}[];
|
|
783
1163
|
failed: {
|
|
784
1164
|
path: string;
|
|
@@ -787,14 +1167,38 @@ interface ScheduleReport {
|
|
|
787
1167
|
}
|
|
788
1168
|
declare class TaskLibraryScheduler {
|
|
789
1169
|
private taskLibrary;
|
|
790
|
-
private
|
|
1170
|
+
private options;
|
|
791
1171
|
private scheduledTasks;
|
|
792
1172
|
constructor(options: Options$1);
|
|
793
1173
|
start(): Promise<ScheduleReport>;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* Stop starting runs, and give the runs in progress up to `graceMs` to end
|
|
1176
|
+
* before closing them as interrupted.
|
|
1177
|
+
*/
|
|
1178
|
+
stop(options?: {
|
|
1179
|
+
graceMs?: number;
|
|
1180
|
+
}): Promise<void>;
|
|
1181
|
+
/** Stop as a process that died would, writing nothing. For tests standing in for a crash. */
|
|
1182
|
+
abandon(): void;
|
|
1183
|
+
/**
|
|
1184
|
+
* Check a loaded task's exports and start its schedule. The timing it is
|
|
1185
|
+
* scheduled on is returned; anything wrong with the exports is thrown,
|
|
1186
|
+
* naming the task, so it fails now rather than on the first tick.
|
|
1187
|
+
*/
|
|
1188
|
+
scheduleTask(task: Task): Promise<TimingExport>;
|
|
1189
|
+
/**
|
|
1190
|
+
* Start one run of a scheduled task now, outside its schedule: retries,
|
|
1191
|
+
* budget, locks and hooks all apply, and errors go where a tick's go. It is
|
|
1192
|
+
* not a tick, so the breaker does not hold it back, and it leaves the next
|
|
1193
|
+
* run where it was. Resolves once the run is under way, not when it ends.
|
|
1194
|
+
*
|
|
1195
|
+
* A run of the task still going, in this process or another, is reported
|
|
1196
|
+
* rather than recorded as a skip: the person asking can simply wait.
|
|
1197
|
+
*/
|
|
1198
|
+
runNow(taskPath: string): Promise<RunNowOutcome>;
|
|
1199
|
+
/** Pause, resume or move a scheduled task's next run, as `ngn pause`, `resume` and `next` do. */
|
|
1200
|
+
control(taskPath: string, command: ScheduleCommand): Promise<ScheduleControlResult>;
|
|
1201
|
+
unscheduleTask(task: Task): Promise<void>;
|
|
798
1202
|
}
|
|
799
1203
|
|
|
800
1204
|
type Options = {
|
|
@@ -807,11 +1211,6 @@ declare class TaskOnceRunner {
|
|
|
807
1211
|
}
|
|
808
1212
|
|
|
809
1213
|
declare function isCronPatternValid(pattern: string): boolean;
|
|
810
|
-
/**
|
|
811
|
-
* Schedule a loaded task on `pattern`. The task's exports are checked here, so
|
|
812
|
-
* a bad `overlap` or `budget` fails now rather than on the first tick.
|
|
813
|
-
*/
|
|
814
|
-
declare function scheduleTask(task: Task, pattern: string, onError: (error: unknown) => void): Cron<undefined>;
|
|
815
1214
|
|
|
816
1215
|
declare function setupDbClient(dbPath: string): Promise<Client>;
|
|
817
1216
|
declare function getDbClient(): Client;
|
|
@@ -824,4 +1223,4 @@ declare function createControlsGenerator({ dbClient, env, report, }: {
|
|
|
824
1223
|
report?: (problem: RunProblem) => void;
|
|
825
1224
|
}): BuildCompiledTaskConfigFn;
|
|
826
1225
|
|
|
827
|
-
export { type AddJob, type Backoff, type ClaimOptions, Compiler, type DBInstance, type Duration, type InitDBOptions, type Job, type JobFilter, type JobStatus, LockBusyError, type LockOptions, type Migration, type OverlapPolicy, Queue, type QueueOptions, type QueueStats, type RunProblem, type ScheduleReport, Task, type TaskContext, TaskLibrary, TaskLibraryScheduler, TaskOnceRunner, type TaskRun, type TaskSqlite, type WorkHandler, type WorkOptions, type WorkSummary, type WorkerHandle, closeTaskDatabases, createControlsGenerator, executeEsm, failedRunId, getDbClient, isCronPatternValid, releaseTaskDatabases, runFailure,
|
|
1226
|
+
export { type AddJob, type Backoff, type ClaimOptions, type Clock, Compiler, type DBInstance, type Duration, type Edge, type ExecuteSchedule, type InitDBOptions, type IntervalTiming, type Job, type JobFilter, type JobStatus, LockBusyError, type LockOptions, type Migration, type NextDecision, type NextDelay, type NextOutcome, type OverlapPolicy, Queue, type QueueOptions, type QueueStats, type ResolvedTiming, type RunNowOutcome, type RunProblem, STOP_GRACE_MS, type ScheduleCommand, type ScheduleControlResult, type ScheduleControlTarget, type ScheduleReport, ScheduleStore, Task, type TaskContext, TaskLibrary, TaskLibraryScheduler, TaskOnceRunner, type TaskRun, type TaskSqlite, type TickOutcome, type TickStatus, type TimingBackoff, type TimingExport, type WorkHandler, type WorkOptions, type WorkSummary, type WorkerHandle, closeTaskDatabases, controlSchedule, createControlsGenerator, executeEsm, failedRunId, getDbClient, isCronPatternValid, releaseTaskDatabases, resolveTiming, runFailure, runOnce, setupDbClient, systemClock, timingFromFlag };
|