@boxxy/dispatch 0.1.1 → 0.1.3
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/attempts/attempt-machine.d.ts +804 -0
- package/dist/attempts/attempt-moves.d.ts +2 -0
- package/dist/attempts/bind-attempt-session.d.ts +14 -0
- package/dist/attempts/build-attempt-change.d.ts +73 -0
- package/dist/attempts/build-attempt-fact.d.ts +3 -0
- package/dist/attempts/end-item-attempts.d.ts +18 -0
- package/dist/attempts/is-same-guard.d.ts +5 -0
- package/dist/attempts/pick-call-attempt.d.ts +10 -0
- package/dist/attempts/stop-stray-session.d.ts +11 -0
- package/dist/attempts/types.d.ts +142 -0
- package/dist/attempts/write-attempt-move.d.ts +7 -0
- package/dist/attempts/write-session-bind.d.ts +3 -0
- package/dist/claims/build-snapshot-hash.d.ts +2 -0
- package/dist/claims/claim-next.d.ts +6 -0
- package/dist/claims/pick-kept-session.d.ts +9 -0
- package/dist/claims/request-wake.d.ts +3 -0
- package/dist/claims/resume-claim.d.ts +3 -0
- package/dist/claims/start-claim.d.ts +3 -0
- package/dist/claims/types.d.ts +44 -0
- package/dist/closeouts/build-settle-plans.d.ts +17 -0
- package/dist/closeouts/close-out-attempt.d.ts +3 -0
- package/dist/closeouts/effect-handlers.d.ts +2 -0
- package/dist/closeouts/pick-settlement.d.ts +14 -0
- package/dist/closeouts/settle-attempt.d.ts +3 -0
- package/dist/closeouts/types.d.ts +50 -0
- package/dist/index.d.ts +38 -0
- package/dist/notices/constants.d.ts +4 -0
- package/dist/notices/create-null-notifier.d.ts +2 -0
- package/dist/notices/send-notices.d.ts +2 -0
- package/dist/notices/types.d.ts +11 -0
- package/dist/outbox/build-backoff-ms.d.ts +2 -0
- package/dist/outbox/build-initial-sender-state.d.ts +2 -0
- package/dist/outbox/build-outbox-status.d.ts +15 -0
- package/dist/outbox/constants.d.ts +2 -0
- package/dist/outbox/measure-wire-bytes.d.ts +2 -0
- package/dist/outbox/pick-sender-move.d.ts +10 -0
- package/dist/outbox/run-sender-once.d.ts +2 -0
- package/dist/outbox/take-batch.d.ts +3 -0
- package/dist/outbox/types.d.ts +61 -0
- package/dist/pull-requests/build-link-change.d.ts +11 -0
- package/dist/pull-requests/build-state-fact.d.ts +8 -0
- package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
- package/dist/pull-requests/collect-seen-state.d.ts +7 -0
- package/dist/pull-requests/constants.d.ts +3 -0
- package/dist/pull-requests/link-pull-request.d.ts +2 -0
- package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
- package/dist/pull-requests/types.d.ts +37 -0
- package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
- package/dist/readings/act-on-reading.d.ts +3 -0
- package/dist/readings/find-waiting-since.d.ts +9 -0
- package/dist/readings/pick-reading-action.d.ts +2 -0
- package/dist/readings/types.d.ts +79 -0
- package/dist/sessions/build-keep-writes.d.ts +9 -0
- package/dist/sessions/build-kept-clear.d.ts +2 -0
- package/dist/sessions/forget-sessions.d.ts +2 -0
- package/dist/sessions/is-kept-due.d.ts +2 -0
- package/dist/sessions/resume-kept-session.d.ts +6 -0
- package/dist/sessions/types.d.ts +19 -0
- package/dist/test-utils/apply-memory-change.d.ts +3 -0
- package/dist/test-utils/build-memory-reader.d.ts +3 -0
- package/dist/test-utils/build-mock-claim-change.d.ts +2 -2
- package/dist/test-utils/build-mock-new-id.d.ts +2 -0
- package/dist/test-utils/build-mock-note-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pull-request-change.d.ts +1 -0
- package/dist/test-utils/{build-mock-link-change.d.ts → build-mock-relation-change.d.ts} +2 -2
- package/dist/test-utils/build-mock-running-reading.d.ts +6 -0
- package/dist/test-utils/build-mock-start-change.d.ts +2 -2
- package/dist/test-utils/claim-store-close-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-copy-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-outbox-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
- package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
- package/dist/test-utils/compare-text.d.ts +1 -0
- package/dist/test-utils/create-fake-backoff.d.ts +2 -0
- package/dist/test-utils/create-fake-clock.d.ts +5 -0
- package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
- package/dist/test-utils/create-fake-forge.d.ts +17 -0
- package/dist/test-utils/create-fake-worker.d.ts +42 -0
- package/dist/test-utils/create-memory-claim-store.d.ts +2 -2
- package/dist/test-utils/create-test-dispatch.d.ts +46 -0
- package/dist/test-utils/forge-contract.d.ts +15 -0
- package/dist/test-utils/is-same-held-seat.d.ts +2 -0
- package/dist/test-utils/types.d.ts +12 -2
- package/dist/test-utils/worker-contract.d.ts +14 -0
- package/dist/timeouts.d.ts +2 -0
- package/dist/types.d.ts +13 -0
- package/package.json +10 -3
- package/src/attempts/attempt-machine.ts +147 -0
- package/src/attempts/attempt-moves.ts +218 -0
- package/src/attempts/bind-attempt-session.ts +61 -0
- package/src/attempts/build-attempt-change.ts +540 -0
- package/src/attempts/build-attempt-fact.ts +135 -0
- package/src/attempts/end-item-attempts.ts +95 -0
- package/src/attempts/is-same-guard.ts +22 -0
- package/src/attempts/pick-call-attempt.ts +32 -0
- package/src/attempts/stop-stray-session.ts +48 -0
- package/src/attempts/types.ts +231 -0
- package/src/attempts/write-attempt-move.ts +41 -0
- package/src/attempts/write-session-bind.ts +47 -0
- package/src/claims/build-snapshot-hash.ts +9 -0
- package/src/claims/claim-next.ts +306 -0
- package/src/claims/pick-kept-session.ts +41 -0
- package/src/claims/request-wake.ts +17 -0
- package/src/claims/resume-claim.ts +56 -0
- package/src/claims/start-claim.ts +99 -0
- package/src/claims/types.ts +67 -0
- package/src/closeouts/build-settle-plans.ts +173 -0
- package/src/closeouts/close-out-attempt.ts +156 -0
- package/src/closeouts/effect-handlers.ts +232 -0
- package/src/closeouts/pick-settlement.ts +80 -0
- package/src/closeouts/settle-attempt.ts +161 -0
- package/src/closeouts/types.ts +87 -0
- package/src/index.ts +61 -0
- package/src/notices/constants.ts +7 -0
- package/src/notices/create-null-notifier.ts +6 -0
- package/src/notices/send-notices.ts +41 -0
- package/src/notices/types.ts +14 -0
- package/src/outbox/build-backoff-ms.ts +14 -0
- package/src/outbox/build-initial-sender-state.ts +14 -0
- package/src/outbox/build-outbox-status.ts +43 -0
- package/src/outbox/constants.ts +15 -0
- package/src/outbox/measure-wire-bytes.ts +31 -0
- package/src/outbox/pick-sender-move.ts +37 -0
- package/src/outbox/run-sender-once.ts +174 -0
- package/src/outbox/take-batch.ts +35 -0
- package/src/outbox/types.ts +76 -0
- package/src/pull-requests/build-link-change.ts +95 -0
- package/src/pull-requests/build-state-fact.ts +39 -0
- package/src/pull-requests/collect-attempt-trailers.ts +20 -0
- package/src/pull-requests/collect-seen-state.ts +19 -0
- package/src/pull-requests/constants.ts +5 -0
- package/src/pull-requests/link-pull-request.ts +119 -0
- package/src/pull-requests/parse-pull-request-url.ts +30 -0
- package/src/pull-requests/types.ts +63 -0
- package/src/pull-requests/watch-pull-requests.ts +308 -0
- package/src/readings/act-on-reading.ts +318 -0
- package/src/readings/find-waiting-since.ts +46 -0
- package/src/readings/pick-reading-action.ts +315 -0
- package/src/readings/types.ts +98 -0
- package/src/sessions/build-keep-writes.ts +55 -0
- package/src/sessions/build-kept-clear.ts +12 -0
- package/src/sessions/forget-sessions.ts +106 -0
- package/src/sessions/is-kept-due.ts +27 -0
- package/src/sessions/resume-kept-session.ts +25 -0
- package/src/sessions/types.ts +39 -0
- package/src/test-utils/apply-memory-change.ts +252 -0
- package/src/test-utils/build-memory-reader.ts +364 -0
- package/src/test-utils/build-mock-bind-change.ts +18 -11
- package/src/test-utils/build-mock-claim-change.ts +18 -8
- package/src/test-utils/build-mock-file-item-change.ts +3 -1
- package/src/test-utils/build-mock-new-id.ts +12 -0
- package/src/test-utils/build-mock-note-change.ts +18 -0
- package/src/test-utils/build-mock-pause-change.ts +2 -10
- package/src/test-utils/build-mock-pull-request-change.ts +19 -9
- package/src/test-utils/{build-mock-link-change.ts → build-mock-relation-change.ts} +6 -6
- package/src/test-utils/build-mock-running-reading.ts +19 -0
- package/src/test-utils/build-mock-settle-change.ts +18 -6
- package/src/test-utils/build-mock-start-change.ts +13 -6
- package/src/test-utils/claim-store-attempt-reader-cases.ts +163 -133
- package/src/test-utils/claim-store-claim-cases.ts +168 -85
- package/src/test-utils/claim-store-close-cases.ts +95 -0
- package/src/test-utils/claim-store-contract.ts +12 -2
- package/src/test-utils/claim-store-copy-cases.ts +100 -0
- package/src/test-utils/claim-store-exclusive-cases.ts +65 -19
- package/src/test-utils/claim-store-guard-cases.ts +92 -82
- package/src/test-utils/claim-store-item-reader-cases.ts +291 -77
- package/src/test-utils/claim-store-outbox-cases.ts +281 -0
- package/src/test-utils/claim-store-race-cases.ts +150 -0
- package/src/test-utils/claim-store-rollback-cases.ts +50 -50
- package/src/test-utils/claim-store-seat-cases.ts +270 -0
- package/src/test-utils/claim-store-session-cases.ts +289 -0
- package/src/test-utils/collect-memory-limit-fills.ts +15 -0
- package/src/test-utils/compare-text.ts +8 -0
- package/src/test-utils/create-fake-backoff.ts +16 -0
- package/src/test-utils/create-fake-clock.ts +20 -0
- package/src/test-utils/create-fake-fact-sink.ts +74 -0
- package/src/test-utils/create-fake-forge.ts +117 -0
- package/src/test-utils/create-fake-worker.ts +303 -0
- package/src/test-utils/create-memory-claim-store.ts +73 -484
- package/src/test-utils/create-test-dispatch.ts +597 -0
- package/src/test-utils/forge-contract.ts +171 -0
- package/src/test-utils/is-same-held-seat.ts +11 -0
- package/src/test-utils/types.ts +14 -2
- package/src/test-utils/worker-contract.ts +192 -0
- package/src/timeouts.ts +17 -0
- package/src/types.ts +32 -0
- package/dist/test-utils/claim-store-pool-cases.d.ts +0 -2
- package/src/test-utils/claim-store-pool-cases.ts +0 -178
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Forge, PullRequestRef } from '@boxxy/model';
|
|
2
|
+
import { brandSchemas } from '@boxxy/model';
|
|
3
|
+
import type { SeededPullRequest } from './create-fake-forge.ts';
|
|
4
|
+
|
|
5
|
+
// What a forge adapter's test gives each case: the forge, and a way to put a pull request on the
|
|
6
|
+
// service behind it.
|
|
7
|
+
export interface ForgeHarness {
|
|
8
|
+
readonly forge: Forge;
|
|
9
|
+
seed: (pullRequest: SeededPullRequest) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ForgeContractOutcome {
|
|
13
|
+
readonly received: unknown;
|
|
14
|
+
readonly expected: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ForgeContractCase = readonly [
|
|
18
|
+
title: string,
|
|
19
|
+
check: (harness: ForgeHarness) => Promise<ForgeContractOutcome>,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const repo = brandSchemas.repoName.parse('acme/lamps');
|
|
23
|
+
const otherRepo = brandSchemas.repoName.parse('acme/shades');
|
|
24
|
+
|
|
25
|
+
// The behaviour of the forge port that dispatch relies on. Every adapter and the fake pass it.
|
|
26
|
+
export const forgeContract: readonly ForgeContractCase[] = [
|
|
27
|
+
[
|
|
28
|
+
'#readPullRequest reads an open pull request with its GitHub id, url and commits',
|
|
29
|
+
async (harness) => {
|
|
30
|
+
harness.seed(buildOpen(7, 2));
|
|
31
|
+
|
|
32
|
+
const reading = await harness.forge.readPullRequest(buildRef(7));
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
received: reading,
|
|
36
|
+
expected: {
|
|
37
|
+
url: 'https://github.com/acme/lamps/pull/7',
|
|
38
|
+
githubId: 5_000_007,
|
|
39
|
+
state: 'open',
|
|
40
|
+
mergedAt: null,
|
|
41
|
+
mergeCommit: null,
|
|
42
|
+
commits: [
|
|
43
|
+
{
|
|
44
|
+
sha: buildSha(1),
|
|
45
|
+
message: 'Fix lamp 1\n\nBoxxy-Attempt: 0190a6f4-0000-7000-8000-000000000001',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
sha: buildSha(2),
|
|
49
|
+
message: 'Fix lamp 2\n\nBoxxy-Attempt: 0190a6f4-0000-7000-8000-000000000001',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'#readPullRequest reads a merged pull request with its merge time and merge commit',
|
|
58
|
+
async (harness) => {
|
|
59
|
+
const mergedAt = brandSchemas.instant.parse(1_800_000_000_000);
|
|
60
|
+
const open = buildOpen(8);
|
|
61
|
+
|
|
62
|
+
harness.seed({
|
|
63
|
+
...open,
|
|
64
|
+
reading: { ...open.reading, state: 'merged', mergedAt, mergeCommit: buildSha(99) },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const reading = await harness.forge.readPullRequest(buildRef(8));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
received:
|
|
71
|
+
reading === 'not-found'
|
|
72
|
+
? reading
|
|
73
|
+
: [reading.state, reading.mergedAt, reading.mergeCommit],
|
|
74
|
+
expected: ['merged', mergedAt, buildSha(99)],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
'#readPullRequest reads a pull request closed with no merge as closed',
|
|
80
|
+
async (harness) => {
|
|
81
|
+
const open = buildOpen(9);
|
|
82
|
+
|
|
83
|
+
harness.seed({ ...open, reading: { ...open.reading, state: 'closed' } });
|
|
84
|
+
|
|
85
|
+
const reading = await harness.forge.readPullRequest(buildRef(9));
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
received:
|
|
89
|
+
reading === 'not-found'
|
|
90
|
+
? reading
|
|
91
|
+
: [reading.state, reading.mergedAt, reading.mergeCommit],
|
|
92
|
+
expected: ['closed', null, null],
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
'#readPullRequest reads every commit of a pull request over one page',
|
|
98
|
+
async (harness) => {
|
|
99
|
+
harness.seed(buildOpen(10, 130));
|
|
100
|
+
|
|
101
|
+
const reading = await harness.forge.readPullRequest(buildRef(10));
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
received: reading === 'not-found' ? reading : reading.commits.map((commit) => commit.sha),
|
|
105
|
+
expected: Array.from({ length: 130 }, (_unused, index) => buildSha(index + 1)),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
'#readPullRequest answers not-found for a pull request that does not exist',
|
|
111
|
+
async (harness) => ({
|
|
112
|
+
received: await harness.forge.readPullRequest(buildRef(404)),
|
|
113
|
+
expected: 'not-found',
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
'#listOpenPullRequests lists the open pull requests of the repo, lowest number first',
|
|
118
|
+
async (harness) => {
|
|
119
|
+
const merged = buildOpen(2);
|
|
120
|
+
|
|
121
|
+
harness.seed(buildOpen(3));
|
|
122
|
+
harness.seed(buildOpen(1));
|
|
123
|
+
harness.seed({ ...merged, reading: { ...merged.reading, state: 'closed' } });
|
|
124
|
+
harness.seed(buildOpen(1, 0, otherRepo));
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
received: await harness.forge.listOpenPullRequests(repo),
|
|
128
|
+
expected: [buildRef(1), buildRef(3)],
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
'#listOpenPullRequests lists every open pull request over one page',
|
|
134
|
+
async (harness) => {
|
|
135
|
+
for (let number = 1; number <= 105; number += 1) {
|
|
136
|
+
harness.seed(buildOpen(number));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const listed = await harness.forge.listOpenPullRequests(repo);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
received: listed.map((ref) => ref.number),
|
|
143
|
+
expected: Array.from({ length: 105 }, (_unused, index) => index + 1),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
function buildOpen(number: number, commitCount = 0, inRepo = repo): SeededPullRequest {
|
|
150
|
+
return {
|
|
151
|
+
ref: buildRef(number, inRepo),
|
|
152
|
+
githubId: 5_000_000 + number,
|
|
153
|
+
reading: {
|
|
154
|
+
state: 'open',
|
|
155
|
+
mergedAt: null,
|
|
156
|
+
mergeCommit: null,
|
|
157
|
+
commits: Array.from({ length: commitCount }, (_unused, index) => ({
|
|
158
|
+
sha: buildSha(index + 1),
|
|
159
|
+
message: `Fix lamp ${index + 1}\n\nBoxxy-Attempt: 0190a6f4-0000-7000-8000-000000000001`,
|
|
160
|
+
})),
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildRef(number: number, inRepo = repo): PullRequestRef {
|
|
166
|
+
return { repo: inRepo, number };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildSha(index: number): ReturnType<typeof brandSchemas.commitSha.parse> {
|
|
170
|
+
return brandSchemas.commitSha.parse(index.toString(16).padStart(40, '0'));
|
|
171
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LimitSeatRow } from '@boxxy/model';
|
|
2
|
+
|
|
3
|
+
// the key of the store's unique index over the held seats
|
|
4
|
+
export function isSameHeldSeat(seat: LimitSeatRow, held: LimitSeatRow): boolean {
|
|
5
|
+
return (
|
|
6
|
+
seat.limit === held.limit &&
|
|
7
|
+
seat.attempt === held.attempt &&
|
|
8
|
+
seat.holdGen === held.holdGen &&
|
|
9
|
+
held.releasedAt === null
|
|
10
|
+
);
|
|
11
|
+
}
|
package/src/test-utils/types.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { ClaimStore } from '@boxxy/model';
|
|
1
|
+
import type { ClaimStore, Limit, LimitSeatRow, OutboxFact, TableName, Tables } from '@boxxy/model';
|
|
2
|
+
|
|
3
|
+
export interface ClosableClaimStore extends ClaimStore {
|
|
4
|
+
close: () => Promise<void>;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
export interface ClaimStoreOutcome {
|
|
4
8
|
readonly received: unknown;
|
|
@@ -7,5 +11,13 @@ export interface ClaimStoreOutcome {
|
|
|
7
11
|
|
|
8
12
|
export type ClaimStoreCase = readonly [
|
|
9
13
|
title: string,
|
|
10
|
-
check: (store:
|
|
14
|
+
check: (store: ClosableClaimStore) => Promise<ClaimStoreOutcome>,
|
|
11
15
|
];
|
|
16
|
+
|
|
17
|
+
// The memory store's tables: one array per table that a row write names, and the ones only the
|
|
18
|
+
// store writes.
|
|
19
|
+
export type MemoryTables = { [T in TableName]: Tables[T][] } & {
|
|
20
|
+
outbox: OutboxFact[];
|
|
21
|
+
limits: Limit[];
|
|
22
|
+
limit_seats: LimitSeatRow[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { AtcSessionId, AttemptId, AttemptRow, SpawnRequest, WorkerKind } from '@boxxy/model';
|
|
2
|
+
import { brandSchemas } from '@boxxy/model';
|
|
3
|
+
import { buildMockV1AttemptRow } from '@boxxy/model/test-utils/build-mock-v1-attempt-row';
|
|
4
|
+
import invariant from 'tiny-invariant';
|
|
5
|
+
|
|
6
|
+
// What a worker adapter's test gives each case: the worker, and a spawn request on a target the
|
|
7
|
+
// test owns, for a new attempt ID each call.
|
|
8
|
+
export interface WorkerHarness {
|
|
9
|
+
readonly worker: WorkerKind;
|
|
10
|
+
buildSpawnRequest: (attempt: AttemptId) => SpawnRequest;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkerContractOutcome {
|
|
14
|
+
readonly received: unknown;
|
|
15
|
+
readonly expected: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type WorkerContractCase = readonly [
|
|
19
|
+
title: string,
|
|
20
|
+
check: (harness: WorkerHarness) => Promise<WorkerContractOutcome>,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// The behaviour of the worker port that dispatch relies on. Every adapter and the fake pass it.
|
|
24
|
+
export const workerContract: readonly WorkerContractCase[] = [
|
|
25
|
+
[
|
|
26
|
+
'#start starts a session that reads as running on the same atc session',
|
|
27
|
+
async (harness) => {
|
|
28
|
+
const attempt = await startAttempt(harness);
|
|
29
|
+
const reading = await harness.worker.reconcile(attempt);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
received: [reading.kind, 'atcSession' in reading ? reading.atcSession : null],
|
|
33
|
+
expected: ['running', attempt.atcSession],
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
'#start gives the same session for a second request with the same idempotency key',
|
|
39
|
+
async (harness) => {
|
|
40
|
+
const id = buildMockV1AttemptRow().id;
|
|
41
|
+
const request = harness.buildSpawnRequest(id);
|
|
42
|
+
|
|
43
|
+
const first = await harness.worker.start(request);
|
|
44
|
+
const second = await harness.worker.start(request);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
received: findStartedSession(second),
|
|
48
|
+
expected: findStartedSession(first),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
'#reconcile reads absent for an attempt whose spawn never ran',
|
|
54
|
+
async (harness) => {
|
|
55
|
+
const id = buildMockV1AttemptRow().id;
|
|
56
|
+
|
|
57
|
+
const reading = await harness.worker.reconcile(
|
|
58
|
+
buildMockV1AttemptRow({
|
|
59
|
+
id,
|
|
60
|
+
atcSession: null,
|
|
61
|
+
spawnRequest: { ...harness.buildSpawnRequest(id) },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return { received: reading, expected: { kind: 'absent' } };
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
'#suspend answers active for a session with activity after the idle time',
|
|
70
|
+
async (harness) => {
|
|
71
|
+
const attempt = await startAttempt(harness);
|
|
72
|
+
const outcome = await harness.worker.suspend(attempt, brandSchemas.instant.parse(0));
|
|
73
|
+
|
|
74
|
+
return { received: outcome, expected: { kind: 'active' } };
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
'#suspend with no idle check stops a running session, which then reads stopped',
|
|
79
|
+
async (harness) => {
|
|
80
|
+
const attempt = await startAttempt(harness);
|
|
81
|
+
const outcome = await harness.worker.suspend(attempt, null);
|
|
82
|
+
const reading = await harness.worker.reconcile(attempt);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
received: [outcome.kind, isStopped(reading.kind)],
|
|
86
|
+
expected: ['suspended', true],
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
'#stop stops a running session, and a second stop is safe',
|
|
92
|
+
async (harness) => {
|
|
93
|
+
const attempt = await startAttempt(harness);
|
|
94
|
+
const first = await harness.worker.stop(attempt);
|
|
95
|
+
const second = await harness.worker.stop(attempt);
|
|
96
|
+
const reading = await harness.worker.reconcile(attempt);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
received: [first.kind, second.kind, isStopped(reading.kind)],
|
|
100
|
+
expected: ['stopped', 'stopped', true],
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
'#resume runs a stopped session again on the same atc session',
|
|
106
|
+
async (harness) => {
|
|
107
|
+
const attempt = await startAttempt(harness);
|
|
108
|
+
|
|
109
|
+
await harness.worker.stop(attempt);
|
|
110
|
+
|
|
111
|
+
const outcome = await harness.worker.resume(attempt);
|
|
112
|
+
|
|
113
|
+
return { received: findStartedSession(outcome), expected: attempt.atcSession };
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
'#forget removes a stopped session, and a second forget answers absent',
|
|
118
|
+
async (harness) => {
|
|
119
|
+
const attempt = await startAttempt(harness);
|
|
120
|
+
|
|
121
|
+
const session = getSession(attempt);
|
|
122
|
+
|
|
123
|
+
await harness.worker.stop(attempt);
|
|
124
|
+
|
|
125
|
+
const first = await harness.worker.forget(session);
|
|
126
|
+
const second = await harness.worker.forget(session);
|
|
127
|
+
const reading = await harness.worker.reconcile(attempt);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
received: [first.kind, second.kind, reading.kind],
|
|
131
|
+
expected: ['forgotten', 'absent', 'absent'],
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
'#resumeKept runs a stopped session again for a new attempt',
|
|
137
|
+
async (harness) => {
|
|
138
|
+
const first = await startAttempt(harness);
|
|
139
|
+
|
|
140
|
+
const session = getSession(first);
|
|
141
|
+
|
|
142
|
+
await harness.worker.stop(first);
|
|
143
|
+
|
|
144
|
+
const second = buildMockV1AttemptRow().id;
|
|
145
|
+
|
|
146
|
+
const outcome = await harness.worker.resumeKept(
|
|
147
|
+
{
|
|
148
|
+
atcSession: session,
|
|
149
|
+
target: harness.buildSpawnRequest(first.id).target,
|
|
150
|
+
attempt: first.id,
|
|
151
|
+
keptAt: brandSchemas.instant.parse(0),
|
|
152
|
+
caller: first.caller,
|
|
153
|
+
},
|
|
154
|
+
harness.buildSpawnRequest(second),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return { received: findStartedSession(outcome), expected: session };
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// An attempt row as the store holds it after the worker started its session.
|
|
163
|
+
async function startAttempt(harness: WorkerHarness): Promise<AttemptRow> {
|
|
164
|
+
const id = buildMockV1AttemptRow().id;
|
|
165
|
+
const request = harness.buildSpawnRequest(id);
|
|
166
|
+
|
|
167
|
+
const outcome = await harness.worker.start(request);
|
|
168
|
+
|
|
169
|
+
return buildMockV1AttemptRow({
|
|
170
|
+
id,
|
|
171
|
+
spawnRequest: { ...request },
|
|
172
|
+
atcSession: findStartedSession(outcome),
|
|
173
|
+
caller: outcome.kind === 'started' ? outcome.caller : null,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function findStartedSession(
|
|
178
|
+
outcome: Awaited<ReturnType<WorkerKind['start']>>,
|
|
179
|
+
): AtcSessionId | null {
|
|
180
|
+
return outcome.kind === 'started' ? outcome.atcSession : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// a stop sleeps the host of an imp target, and ends the process of a local one
|
|
184
|
+
function isStopped(kind: string): boolean {
|
|
185
|
+
return kind === 'suspended' || kind === 'ended';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getSession(attempt: AttemptRow): AtcSessionId {
|
|
189
|
+
invariant(attempt.atcSession !== null, 'the worker started a session');
|
|
190
|
+
|
|
191
|
+
return attempt.atcSession;
|
|
192
|
+
}
|
package/src/timeouts.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DispatchTimeouts } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export const timeouts: DispatchTimeouts = {
|
|
4
|
+
spawnLostMinutes: 10,
|
|
5
|
+
resumeLostMinutes: 10,
|
|
6
|
+
closingGiveUpMinutes: 10,
|
|
7
|
+
stopGraceSeconds: 60,
|
|
8
|
+
|
|
9
|
+
// two ticks of the runner's 5-second loop: the reconcile table reads the time, not a tick count
|
|
10
|
+
seatlessSeconds: 10,
|
|
11
|
+
capacityBackoffSeconds: 60,
|
|
12
|
+
stallMinutes: 30,
|
|
13
|
+
keptSessionHours: 24,
|
|
14
|
+
failedSessionHours: 24,
|
|
15
|
+
usageLimitBackoffHours: 24,
|
|
16
|
+
strayHours: 24,
|
|
17
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Every time limit of dispatch that the rules file does not hold.
|
|
2
|
+
export interface DispatchTimeouts {
|
|
3
|
+
// `starting` or `reconciling` with no session
|
|
4
|
+
readonly spawnLostMinutes: number;
|
|
5
|
+
|
|
6
|
+
// `waking` with no running session
|
|
7
|
+
readonly resumeLostMinutes: number;
|
|
8
|
+
|
|
9
|
+
// `closing` with a session that does not stop
|
|
10
|
+
readonly closingGiveUpMinutes: number;
|
|
11
|
+
|
|
12
|
+
// a `closing` session inside a turn is not stopped before this
|
|
13
|
+
readonly stopGraceSeconds: number;
|
|
14
|
+
|
|
15
|
+
// a session that runs on a `sleeping` attempt wakes over the cap this long after its request
|
|
16
|
+
readonly seatlessSeconds: number;
|
|
17
|
+
|
|
18
|
+
// a pool gives no pick this long after the worker refused a session for capacity
|
|
19
|
+
readonly capacityBackoffSeconds: number;
|
|
20
|
+
|
|
21
|
+
// agent mode: a session with no activity this long is stopped
|
|
22
|
+
readonly stallMinutes: number;
|
|
23
|
+
readonly keptSessionHours: number;
|
|
24
|
+
readonly failedSessionHours: number;
|
|
25
|
+
|
|
26
|
+
// the most a pool waits after `usage-limit`, and the wait when the reading's reset time is not
|
|
27
|
+
// in the future
|
|
28
|
+
readonly usageLimitBackoffHours: number;
|
|
29
|
+
|
|
30
|
+
// how long the runner keeps stopping a stray session
|
|
31
|
+
readonly strayHours: number;
|
|
32
|
+
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { brandSchemas } from '@boxxy/model';
|
|
2
|
-
import { buildMockAttemptRow } from '@boxxy/model/test-utils/build-mock-attempt-row';
|
|
3
|
-
import { buildMockItemRow } from '@boxxy/model/test-utils/build-mock-item-row';
|
|
4
|
-
import { buildMockClaimChange } from './build-mock-claim-change.ts';
|
|
5
|
-
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
6
|
-
import { buildMockSettleChange } from './build-mock-settle-change.ts';
|
|
7
|
-
import type { ClaimStoreCase } from './types.ts';
|
|
8
|
-
|
|
9
|
-
export const claimStorePoolCases: readonly ClaimStoreCase[] = [
|
|
10
|
-
[
|
|
11
|
-
'#syncPoolSlots adds a row for each slot of each pool',
|
|
12
|
-
async (store) => {
|
|
13
|
-
await store.exclusive((transaction) =>
|
|
14
|
-
transaction.syncPoolSlots([
|
|
15
|
-
{ name: brandSchemas.poolName.parse('build'), slots: 2 },
|
|
16
|
-
{ name: brandSchemas.poolName.parse('review'), slots: 1 },
|
|
17
|
-
]),
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const slots: unknown = await store.read((transaction) => transaction.listPoolSlots());
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
received: slots,
|
|
24
|
-
expected: [
|
|
25
|
-
{ pool: 'build', slot: 1, holder: null, gen: 0, retired: false },
|
|
26
|
-
{ pool: 'build', slot: 2, holder: null, gen: 0, retired: false },
|
|
27
|
-
{ pool: 'review', slot: 1, holder: null, gen: 0, retired: false },
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
[
|
|
33
|
-
'#syncPoolSlots retires the rows above a smaller count and deletes none',
|
|
34
|
-
async (store) => {
|
|
35
|
-
const pool = brandSchemas.poolName.parse('build');
|
|
36
|
-
|
|
37
|
-
await store.exclusive(async (transaction) => {
|
|
38
|
-
await transaction.syncPoolSlots([{ name: pool, slots: 3 }]);
|
|
39
|
-
await transaction.syncPoolSlots([{ name: pool, slots: 1 }]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
43
|
-
|
|
44
|
-
const retired: unknown = slots.map((slot) => [slot.slot, slot.retired]);
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
received: retired,
|
|
48
|
-
expected: [
|
|
49
|
-
[1, false],
|
|
50
|
-
[2, true],
|
|
51
|
-
[3, true],
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
[
|
|
57
|
-
'#syncPoolSlots retires every row of a pool the rules no longer name',
|
|
58
|
-
async (store) => {
|
|
59
|
-
const build = brandSchemas.poolName.parse('build');
|
|
60
|
-
const review = brandSchemas.poolName.parse('review');
|
|
61
|
-
|
|
62
|
-
await store.exclusive(async (transaction) => {
|
|
63
|
-
await transaction.syncPoolSlots([
|
|
64
|
-
{ name: build, slots: 1 },
|
|
65
|
-
{ name: review, slots: 2 },
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
|
-
await transaction.syncPoolSlots([{ name: build, slots: 1 }]);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
72
|
-
|
|
73
|
-
const retired: unknown = slots.map((slot) => [slot.pool, slot.slot, slot.retired]);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
received: retired,
|
|
77
|
-
expected: [
|
|
78
|
-
['build', 1, false],
|
|
79
|
-
['review', 1, true],
|
|
80
|
-
['review', 2, true],
|
|
81
|
-
],
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
[
|
|
86
|
-
'#syncPoolSlots leaves the holder on a slot it retires',
|
|
87
|
-
async (store) => {
|
|
88
|
-
const first = buildMockItemRow();
|
|
89
|
-
const second = buildMockItemRow();
|
|
90
|
-
const lower = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
91
|
-
const upper = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
92
|
-
|
|
93
|
-
await store.exclusive(async (transaction) => {
|
|
94
|
-
await transaction.syncPoolSlots([{ name: lower.pool, slots: 2 }]);
|
|
95
|
-
await transaction.applyChange(buildMockFileItemChange(first));
|
|
96
|
-
await transaction.applyChange(buildMockFileItemChange(second));
|
|
97
|
-
await transaction.applyChange(buildMockClaimChange(lower));
|
|
98
|
-
await transaction.applyChange(buildMockClaimChange(upper));
|
|
99
|
-
await transaction.syncPoolSlots([{ name: lower.pool, slots: 1 }]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
received: slots[1],
|
|
106
|
-
expected: {
|
|
107
|
-
pool: upper.pool,
|
|
108
|
-
slot: 2,
|
|
109
|
-
holder: upper.id,
|
|
110
|
-
gen: 1,
|
|
111
|
-
retired: true,
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
[
|
|
117
|
-
'#applyChange never takes a retired slot',
|
|
118
|
-
async (store) => {
|
|
119
|
-
const first = buildMockItemRow();
|
|
120
|
-
const second = buildMockItemRow();
|
|
121
|
-
const held = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
122
|
-
const refused = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
123
|
-
|
|
124
|
-
await store.exclusive(async (transaction) => {
|
|
125
|
-
await transaction.syncPoolSlots([{ name: held.pool, slots: 2 }]);
|
|
126
|
-
await transaction.syncPoolSlots([{ name: held.pool, slots: 1 }]);
|
|
127
|
-
await transaction.applyChange(buildMockFileItemChange(first));
|
|
128
|
-
await transaction.applyChange(buildMockFileItemChange(second));
|
|
129
|
-
await transaction.applyChange(buildMockClaimChange(held));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const result = await store.exclusive((transaction) =>
|
|
133
|
-
transaction.applyChange(buildMockClaimChange(refused)),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
return { received: result, expected: { kind: 'no-free-slot' } };
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
[
|
|
140
|
-
'#syncPoolSlots reuses a retired row with its gen when the pool grows again',
|
|
141
|
-
async (store) => {
|
|
142
|
-
const first = buildMockItemRow();
|
|
143
|
-
const second = buildMockItemRow();
|
|
144
|
-
const third = buildMockItemRow();
|
|
145
|
-
const lower = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
146
|
-
const upper = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
147
|
-
const later = buildMockAttemptRow({ item: third.id, state: 'starting' });
|
|
148
|
-
|
|
149
|
-
await store.exclusive(async (transaction) => {
|
|
150
|
-
await transaction.syncPoolSlots([{ name: lower.pool, slots: 2 }]);
|
|
151
|
-
await transaction.applyChange(buildMockFileItemChange(first));
|
|
152
|
-
await transaction.applyChange(buildMockFileItemChange(second));
|
|
153
|
-
await transaction.applyChange(buildMockFileItemChange(third));
|
|
154
|
-
await transaction.applyChange(buildMockClaimChange(lower));
|
|
155
|
-
await transaction.applyChange(buildMockClaimChange(upper));
|
|
156
|
-
|
|
157
|
-
await transaction.applyChange(
|
|
158
|
-
buildMockSettleChange({ attempt: { ...upper, slot: 2, slotGen: 1 }, state: 'failed' }),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
await transaction.syncPoolSlots([{ name: lower.pool, slots: 1 }]);
|
|
162
|
-
await transaction.syncPoolSlots([{ name: lower.pool, slots: 2 }]);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const result = await store.exclusive((transaction) =>
|
|
166
|
-
transaction.applyChange(buildMockClaimChange(later)),
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
received: result,
|
|
171
|
-
expected: {
|
|
172
|
-
kind: 'applied',
|
|
173
|
-
slot: { pool: later.pool, slot: 2, gen: 2 },
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
];
|