@elaraai/e3-types 1.0.84 → 1.0.86
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/src/api.d.ts +523 -17
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +208 -22
- package/dist/src/api.js.map +1 -1
- package/dist/src/dataflow.d.ts +115 -12
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js +151 -15
- package/dist/src/dataflow.js.map +1 -1
- package/dist/src/dataflow.spec.js +41 -7
- package/dist/src/dataflow.spec.js.map +1 -1
- package/dist/src/dataset-blob.d.ts +19 -2
- package/dist/src/dataset-blob.d.ts.map +1 -1
- package/dist/src/dataset-blob.js +100 -21
- package/dist/src/dataset-blob.js.map +1 -1
- package/dist/src/dataset-blob.spec.js +132 -0
- package/dist/src/dataset-blob.spec.js.map +1 -1
- package/dist/src/execution.d.ts +68 -8
- package/dist/src/execution.d.ts.map +1 -1
- package/dist/src/execution.js +49 -8
- package/dist/src/execution.js.map +1 -1
- package/dist/src/execution.spec.js +31 -9
- package/dist/src/execution.spec.js.map +1 -1
- package/dist/src/index.d.ts +8 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +14 -11
- package/dist/src/index.js.map +1 -1
- package/dist/src/names.d.ts +9 -0
- package/dist/src/names.d.ts.map +1 -1
- package/dist/src/names.js.map +1 -1
- package/dist/src/pieces.d.ts +47 -0
- package/dist/src/pieces.d.ts.map +1 -0
- package/dist/src/pieces.js +17 -0
- package/dist/src/pieces.js.map +1 -0
- package/dist/src/pieces.spec.d.ts +6 -0
- package/dist/src/pieces.spec.d.ts.map +1 -0
- package/dist/src/pieces.spec.js +20 -0
- package/dist/src/pieces.spec.js.map +1 -0
- package/dist/src/release.d.ts +1 -1
- package/dist/src/release.js +1 -1
- package/dist/src/repository.d.ts +16 -0
- package/dist/src/repository.d.ts.map +1 -1
- package/dist/src/repository.js +15 -0
- package/dist/src/repository.js.map +1 -1
- package/dist/src/structure.d.ts +27 -0
- package/dist/src/structure.d.ts.map +1 -1
- package/dist/src/structure.js +59 -18
- package/dist/src/structure.js.map +1 -1
- package/dist/src/structure.spec.d.ts +6 -0
- package/dist/src/structure.spec.d.ts.map +1 -0
- package/dist/src/structure.spec.js +57 -0
- package/dist/src/structure.spec.js.map +1 -0
- package/package.json +3 -2
- package/src/api.ts +225 -21
- package/src/dataflow.spec.ts +49 -7
- package/src/dataflow.ts +160 -14
- package/src/dataset-blob.spec.ts +147 -0
- package/src/dataset-blob.ts +97 -20
- package/src/execution.spec.ts +31 -9
- package/src/execution.ts +52 -7
- package/src/index.ts +38 -2
- package/src/names.ts +12 -0
- package/src/pieces.spec.ts +23 -0
- package/src/pieces.ts +49 -0
- package/src/release.ts +1 -1
- package/src/repository.ts +18 -0
- package/src/structure.spec.ts +65 -0
- package/src/structure.ts +64 -21
package/src/dataflow.spec.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { describe, it } from 'node:test';
|
|
14
14
|
import assert from 'node:assert/strict';
|
|
15
|
-
import { ArrayType, DictType, StringType, StructType, encodeBeast2For, none, some, variant } from '@elaraai/east';
|
|
16
|
-
import { decodeDataflowExecutionState } from './dataflow.js';
|
|
15
|
+
import { ArrayType, DictType, StringType, StructType, encodeBeast2For, equalFor, none, printFor, some, variant } from '@elaraai/east';
|
|
16
|
+
import { DataflowForceType, decodeDataflowExecutionState } from './dataflow.js';
|
|
17
17
|
import { E3_RELEASE } from './release.js';
|
|
18
18
|
|
|
19
19
|
/** Version 1, written by the released e3: task `double` completed, `sum`
|
|
@@ -150,8 +150,9 @@ const STATE_V5 = Buffer.from(
|
|
|
150
150
|
);
|
|
151
151
|
|
|
152
152
|
/** The version 5 state, as release 1.0.79 writes it: the release in place of
|
|
153
|
-
* the version.
|
|
154
|
-
|
|
153
|
+
* the version. Its `force` is a Boolean, which a repository upgrade carries
|
|
154
|
+
* forward (`dataflow-force-tasks`). */
|
|
155
|
+
const STATE_1_0_79 = Buffer.from(
|
|
155
156
|
'iUVhc3QNCgUAngsoKQEFBAAIAgRub25lAwRzb21lAAoACQUEbmFtZQAEaGFzaAAGaW5wdXRzBQZvdXRwdXQACWRlcGVuZHNP' +
|
|
156
157
|
'bgUKBgkBBXRhc2tzBwgCBG5vbmUDBHNvbWUICAIEbm9uZQMEc29tZQICCAIEbm9uZQMEc29tZQsIAgRub25lAwRzb21lAQkC' +
|
|
157
158
|
'CmlucHV0c0hhc2gAC2V4ZWN1dGlvbklkAAgCBG5vbmUDBHNvbWUOCQsEbmFtZQAGc3RhdHVzAAZjYWNoZWQKCm91dHB1dEhh' +
|
|
@@ -180,6 +181,37 @@ const STATE = Buffer.from(
|
|
|
180
181
|
'base64',
|
|
181
182
|
);
|
|
182
183
|
|
|
184
|
+
/** The release 1.0.79 state as release 1.0.85 writes it: `force` names the
|
|
185
|
+
* tasks the run forces, `double`. */
|
|
186
|
+
const STATE = Buffer.from(
|
|
187
|
+
'iUVhc3QNCgUAsgspKgEFAAoACAMDYWxsAgRub25lAgV0YXNrcwMIAgRub25lAgRzb21lAAkFBG5hbWUABGhhc2gABmlucHV0' +
|
|
188
|
+
'cwMGb3V0cHV0AAlkZXBlbmRzT24DCgYJAQV0YXNrcwcIAgRub25lAgRzb21lCAQIAgRub25lAgRzb21lCgIIAgRub25lAgRz' +
|
|
189
|
+
'b21lDAgCBG5vbmUCBHNvbWUBCQIKaW5wdXRzSGFzaAALZXhlY3V0aW9uSWQACAIEbm9uZQIEc29tZQ8JCwRuYW1lAAZzdGF0' +
|
|
190
|
+
'dXMABmNhY2hlZAsKb3V0cHV0SGFzaAUFZXJyb3IFCGV4aXRDb2RlDQlzdGFydGVkQXQOC2NvbXBsZXRlZEF0DghkdXJhdGlv' +
|
|
191
|
+
'bg0EcGxhbgUJZXhlY3V0aW9uEAsAEQsAAAsAEwkDA3NlcQwJdGltZXN0YW1wAQZyZWFzb24FCQgDc2VxDAl0aW1lc3RhbXAB' +
|
|
192
|
+
'B3N1Y2Nlc3MKCGV4ZWN1dGVkDAZjYWNoZWQMBmZhaWxlZAwHc2tpcHBlZAwIZHVyYXRpb24MCQQDc2VxDAl0aW1lc3RhbXAB' +
|
|
193
|
+
'C2V4ZWN1dGlvbklkAAp0b3RhbFRhc2tzDAkFA3NlcQwJdGltZXN0YW1wAQRwYXRoAAxwcmV2aW91c0hhc2gAB25ld0hhc2gA' +
|
|
194
|
+
'CQcDc2VxDAl0aW1lc3RhbXABBHRhc2sABmNhY2hlZAoKb3V0cHV0SGFzaAAIZHVyYXRpb24MCXBlYWtCeXRlcw0JBANzZXEM' +
|
|
195
|
+
'CXRpbWVzdGFtcAEEdGFzawAMY29uZmxpY3RQYXRoAAkGA3NlcQwJdGltZXN0YW1wAQR0YXNrAAVlcnJvcgUIZXhpdENvZGUN' +
|
|
196
|
+
'CGR1cmF0aW9uDAkEA3NlcQwJdGltZXN0YW1wAQR0YXNrAAZyZWFzb24ACQUDc2VxDAl0aW1lc3RhbXABBHRhc2sABWxldmVs' +
|
|
197
|
+
'DAZsZXZlbHMMCQYDc2VxDAl0aW1lc3RhbXABBHRhc2sABWxldmVsDAZsZXZlbHMMBXVuaXRzDAkDA3NlcQwJdGltZXN0YW1w' +
|
|
198
|
+
'AQR0YXNrAAkEA3NlcQwJdGltZXN0YW1wAQR0YXNrAAVjYXVzZQAJBANzZXEMCXRpbWVzdGFtcAEEdGFzawAGcGllY2VzDAkC' +
|
|
199
|
+
'BWxldmVsDAZsZXZlbHMMCAIEbm9uZQIEc29tZSIJAwVtZXJnZSMFaW5kZXgMBXVuaXRzDAgDBmJ1ZGdldAIDY2FwAgdtYWNo' +
|
|
200
|
+
'aW5lAgkHA3NlcQwJdGltZXN0YW1wAQR0YXNrAAR1bml0JAZyZWFzb24lBHBlYWsMCHJlc2VydmVzDAgPE2V4ZWN1dGlvbl9j' +
|
|
201
|
+
'YW5jZWxsZWQVE2V4ZWN1dGlvbl9jb21wbGV0ZWQWEWV4ZWN1dGlvbl9zdGFydGVkFw1pbnB1dF9jaGFuZ2VkGA50YXNrX2Nv' +
|
|
202
|
+
'bXBsZXRlZBkNdGFza19kZWZlcnJlZBoLdGFza19mYWlsZWQbEHRhc2tfaW52YWxpZGF0ZWQcFHRhc2tfbWVyZ2VfY29tcGxl' +
|
|
203
|
+
'dGVkHRJ0YXNrX21lcmdlX3N0YXJ0ZWQeCnRhc2tfcmVhZHkfDHRhc2tfc2tpcHBlZCAKdGFza19zcGxpdCEMdGFza19zdGFy' +
|
|
204
|
+
'dGVkHw11bml0X3JlcXVldWVkJgonCRcHcmVsZWFzZQACaWQABHJlcG8ACXdvcmtzcGFjZQAJc3RhcnRlZEF0AQVmb3JjZQQG' +
|
|
205
|
+
'ZmlsdGVyBQVncmFwaAkJZ3JhcGhIYXNoBQV0YXNrcxIIZXhlY3V0ZWQMBmNhY2hlZAwGZmFpbGVkDAdza2lwcGVkDAZzdGF0' +
|
|
206
|
+
'dXMAC2NvbXBsZXRlZEF0DgVlcnJvcgUOdmVyc2lvblZlY3RvcnMUDWlucHV0U25hcHNob3QTD3Rhc2tPdXRwdXRQYXRocwMK' +
|
|
207
|
+
'cmVleGVjdXRlZAwGZXZlbnRzKAhldmVudFNlcQwBAAHvBpkCYzPUM9CzMFUxMLQ0SDRIMtC1AAJdcwMDIAtEADEcsBSlFuQz' +
|
|
208
|
+
'lRc3PDl96n0aEwMjW0p+aVJOKgMDIwMTlO2QSCFgYOTUy8wrKC0p1qtg4NUrSSzOLtaD2cNcXJrrkEIhYGBEM5YLygUajuQn' +
|
|
209
|
+
'BrifoBRncn5uQU5qSWoKIwOjQzKFABhoFz6DwpFxQfMZECUBNDSJQkBMNBqCAhGEuTPz4guK8tOLUouLQd5l/DAJ5BAggynV' +
|
|
210
|
+
'FOh7oBB7UWleXmZeOkgaOWIc0igEQONRIwE5DoD2MjFBEhlx6ZKXBRKU0IhiYYMEKSwqKY4rCcaGhoYEXg5IAIHCjocLwWbh' +
|
|
211
|
+
'4zmwCMZhYGJhACqeBtIxjZGTb8ImmAwTExMDHwA=',
|
|
212
|
+
'base64',
|
|
213
|
+
);
|
|
214
|
+
|
|
183
215
|
const NEW_FORM = 'a change to the execution state\'s type is a new form: ship a repository upgrade step that carries a repository\'s states into it, move this state to the refusals, and add one the new form writes';
|
|
184
216
|
|
|
185
217
|
describe('decodeDataflowExecutionState', () => {
|
|
@@ -195,7 +227,9 @@ describe('decodeDataflowExecutionState', () => {
|
|
|
195
227
|
it('reads a state in this release\'s form', () => {
|
|
196
228
|
assert.doesNotThrow(() => decodeDataflowExecutionState(STATE), NEW_FORM);
|
|
197
229
|
const state = decodeDataflowExecutionState(STATE);
|
|
198
|
-
assert.equal(state.release, '1.0.
|
|
230
|
+
assert.equal(state.release, '1.0.85');
|
|
231
|
+
const forced = variant('tasks', ['double']);
|
|
232
|
+
assert.ok(equalFor(DataflowForceType)(state.force, forced), `it forces double, not ${printFor(DataflowForceType)(state.force)}`);
|
|
199
233
|
assert.deepEqual(state.tasks.get('sum')!.plan, some('e5'));
|
|
200
234
|
assert.deepEqual(state.tasks.get('sum')!.execution, none);
|
|
201
235
|
assert.deepEqual(state.tasks.get('double')!.execution, some({ inputsHash: 'b'.repeat(64), executionId: '0190a0b0-8888-7000-8000-000000000001' }));
|
|
@@ -211,6 +245,13 @@ describe('decodeDataflowExecutionState', () => {
|
|
|
211
245
|
});
|
|
212
246
|
});
|
|
213
247
|
|
|
248
|
+
it('refuses a state an earlier release wrote, which a repository upgrade carries forward: release 1.0.79\'s Boolean force', () => {
|
|
249
|
+
assert.throws(
|
|
250
|
+
() => decodeDataflowExecutionState(STATE_1_0_79),
|
|
251
|
+
{ message: `the execution state was written by e3 1.0.79, in a form this e3, ${E3_RELEASE}, does not read — re-create the repository: deploy again and import its data again` },
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
214
255
|
it('refuses a state a newer release wrote, naming it', () => {
|
|
215
256
|
const newer = encodeBeast2For(StructType({ release: StringType, id: StringType }))({ release: '999.0.0', id: '7' });
|
|
216
257
|
assert.throws(
|
|
@@ -229,8 +270,9 @@ describe('decodeDataflowExecutionState', () => {
|
|
|
229
270
|
);
|
|
230
271
|
});
|
|
231
272
|
|
|
232
|
-
it('refuses data that is not an execution state', () => {
|
|
273
|
+
it('refuses data that is not an execution state: a value of another type, or bytes that are not beast2', () => {
|
|
233
274
|
const other = encodeBeast2For(StructType({ id: StringType }))({ id: '7' });
|
|
234
|
-
assert.throws(() => decodeDataflowExecutionState(other),
|
|
275
|
+
assert.throws(() => decodeDataflowExecutionState(other), { message: 'the data is not an execution state: its type is not the execution state\'s' });
|
|
276
|
+
assert.throws(() => decodeDataflowExecutionState(new TextEncoder().encode('not a run')), /^Error: the data is not an execution state: it is not beast2 \(/);
|
|
235
277
|
});
|
|
236
278
|
});
|
package/src/dataflow.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Key design decisions:
|
|
13
13
|
* - Dates are Date objects (via DateTimeType), not strings
|
|
14
|
-
* -
|
|
14
|
+
* - A run's events are kept apart from its state, so a write of the state
|
|
15
|
+
* costs what the run holds, not what it has done
|
|
15
16
|
* - Tasks stored as Dict (not Map) for beast2 compatibility
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -30,8 +31,11 @@ import {
|
|
|
30
31
|
decodeBeast2,
|
|
31
32
|
decodeBeast2For,
|
|
32
33
|
isTypeValueEqual,
|
|
34
|
+
lessFor,
|
|
33
35
|
readBeast2Type,
|
|
34
36
|
toEastTypeValue,
|
|
37
|
+
variant,
|
|
38
|
+
type EastTypeValue,
|
|
35
39
|
} from '@elaraai/east';
|
|
36
40
|
import { E3_RELEASE, compareReleases } from './release.js';
|
|
37
41
|
|
|
@@ -157,12 +161,13 @@ export type RequeueReason = ValueTypeOf<typeof RequeueReasonType>;
|
|
|
157
161
|
/**
|
|
158
162
|
* Execution events (VariantType for discriminated union).
|
|
159
163
|
*
|
|
160
|
-
* Events track the progress of a dataflow execution
|
|
161
|
-
*
|
|
164
|
+
* Events track the progress of a dataflow execution. A store keeps a run's
|
|
165
|
+
* events apart from its state, each numbered by its `seq`, and a poll reads
|
|
166
|
+
* those past its cursor.
|
|
162
167
|
*
|
|
163
168
|
* @remarks
|
|
164
|
-
*
|
|
165
|
-
*
|
|
169
|
+
* A stored form, as the state is: a reader decodes events against the whole
|
|
170
|
+
* type they were written with, so a new event is a new form of a run's events,
|
|
166
171
|
* which the release that makes it carries repositories into with an upgrade
|
|
167
172
|
* step. Each unit's progress is a callback, {@link PartitionProgress}, and is
|
|
168
173
|
* not persisted.
|
|
@@ -393,6 +398,59 @@ export interface PartitionProgress {
|
|
|
393
398
|
duration?: number;
|
|
394
399
|
}
|
|
395
400
|
|
|
401
|
+
// =============================================================================
|
|
402
|
+
// Forced Tasks
|
|
403
|
+
// =============================================================================
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The tasks a dataflow run forces: re-executes even where the execution cache
|
|
407
|
+
* holds their results.
|
|
408
|
+
*
|
|
409
|
+
* - `none`: no task; each is served from the cache when it can be
|
|
410
|
+
* - `all`: every task the run launches — under a filter, the filter's task
|
|
411
|
+
* - `tasks`: the tasks named, and no other
|
|
412
|
+
*
|
|
413
|
+
* @remarks
|
|
414
|
+
* A forced task is forced each time the run launches it, so one invalidated
|
|
415
|
+
* mid-run and launched again is forced again. Its dependents are not forced:
|
|
416
|
+
* they re-run when its output changes. A run that names a task the graph does
|
|
417
|
+
* not have, or one its filter's run set leaves out, is refused before anything
|
|
418
|
+
* runs.
|
|
419
|
+
*/
|
|
420
|
+
export const DataflowForceType = VariantType({
|
|
421
|
+
none: NullType,
|
|
422
|
+
all: NullType,
|
|
423
|
+
tasks: ArrayType(StringType),
|
|
424
|
+
});
|
|
425
|
+
export type DataflowForce = ValueTypeOf<typeof DataflowForceType>;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The tasks a run forces, from the option a caller passes it.
|
|
429
|
+
*
|
|
430
|
+
* @param force - `true` for every task, the names of the tasks, or `false` or
|
|
431
|
+
* nothing for none
|
|
432
|
+
* @returns What the run forces
|
|
433
|
+
*/
|
|
434
|
+
export function dataflowForce(force: boolean | readonly string[] | undefined): DataflowForce {
|
|
435
|
+
if (force === undefined || force === false) return variant('none', null);
|
|
436
|
+
if (force === true) return variant('all', null);
|
|
437
|
+
return variant('tasks', [...force]);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The option a run's forced tasks are passed to an orchestrator as.
|
|
442
|
+
*
|
|
443
|
+
* @param force - What the run forces
|
|
444
|
+
* @returns `true` for every task, the names of the tasks, or `false` for none
|
|
445
|
+
*/
|
|
446
|
+
export function dataflowForceOption(force: DataflowForce): boolean | string[] {
|
|
447
|
+
switch (force.type) {
|
|
448
|
+
case 'none': return false;
|
|
449
|
+
case 'all': return true;
|
|
450
|
+
case 'tasks': return [...force.value];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
396
454
|
// =============================================================================
|
|
397
455
|
// Main Execution State
|
|
398
456
|
// =============================================================================
|
|
@@ -404,7 +462,9 @@ export interface PartitionProgress {
|
|
|
404
462
|
*
|
|
405
463
|
* @remarks
|
|
406
464
|
* - Tasks are stored as a Dict (serializes as object, not array of tuples)
|
|
407
|
-
* -
|
|
465
|
+
* - A run's events are kept apart from it: `events` holds those the run has
|
|
466
|
+
* added since its state was last written, which the write hands its store,
|
|
467
|
+
* so a state a store holds, or reads back, holds none
|
|
408
468
|
* - Dates are Date objects (via DateTimeType)
|
|
409
469
|
* - Read it back with {@link decodeDataflowExecutionState}, which reads this
|
|
410
470
|
* form alone. A release that changes it ships a repository upgrade step,
|
|
@@ -426,9 +486,10 @@ export const DataflowExecutionStateType = StructType({
|
|
|
426
486
|
startedAt: DateTimeType,
|
|
427
487
|
|
|
428
488
|
// Config (immutable after initialization)
|
|
429
|
-
/**
|
|
430
|
-
|
|
431
|
-
|
|
489
|
+
/** The tasks the run re-executes even where the cache holds their results
|
|
490
|
+
* ({@link DataflowForceType}) */
|
|
491
|
+
force: DataflowForceType,
|
|
492
|
+
/** One task's exact name: the run runs it and its dependency closure */
|
|
432
493
|
filter: OptionType(StringType),
|
|
433
494
|
|
|
434
495
|
// Graph (inline or by reference)
|
|
@@ -469,10 +530,13 @@ export const DataflowExecutionStateType = StructType({
|
|
|
469
530
|
/** Number of tasks re-executed due to input changes */
|
|
470
531
|
reexecuted: IntegerType,
|
|
471
532
|
|
|
472
|
-
// Events
|
|
473
|
-
/**
|
|
533
|
+
// Events, kept apart
|
|
534
|
+
/** The events the run has added since its state was last written, in
|
|
535
|
+
* order: a write of the state hands them to its store, which keeps a run's
|
|
536
|
+
* events apart from its state, so a stored state holds none */
|
|
474
537
|
events: ArrayType(ExecutionEventType),
|
|
475
|
-
/**
|
|
538
|
+
/** The sequence number of the run's last event, which the next event's
|
|
539
|
+
* follows: 0 while it has none */
|
|
476
540
|
eventSeq: IntegerType,
|
|
477
541
|
});
|
|
478
542
|
export type DataflowExecutionState = ValueTypeOf<typeof DataflowExecutionStateType>;
|
|
@@ -493,10 +557,16 @@ const decodeState = decodeBeast2For(DataflowExecutionStateType);
|
|
|
493
557
|
* @param data - the stored state
|
|
494
558
|
* @returns the state
|
|
495
559
|
* @throws {Error} When the state is of another form, naming the release that
|
|
496
|
-
* wrote it, or the data is not an execution state
|
|
560
|
+
* wrote it, or the data is not an execution state — of another type, or not
|
|
561
|
+
* beast2 at all.
|
|
497
562
|
*/
|
|
498
563
|
export function decodeDataflowExecutionState(data: Uint8Array): DataflowExecutionState {
|
|
499
|
-
|
|
564
|
+
let type: EastTypeValue;
|
|
565
|
+
try {
|
|
566
|
+
type = readBeast2Type(data);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
throw new Error(`the data is not an execution state: it is not beast2 (${err instanceof Error ? err.message : `${err as string}`})`, { cause: err });
|
|
569
|
+
}
|
|
500
570
|
if (isTypeValueEqual(type, STATE_TYPE)) return decodeState(data);
|
|
501
571
|
const { value } = decodeBeast2(data);
|
|
502
572
|
const state = typeof value === 'object' && value !== null ? value as Record<string, unknown> : null;
|
|
@@ -515,6 +585,82 @@ export function decodeDataflowExecutionState(data: Uint8Array): DataflowExecutio
|
|
|
515
585
|
throw new Error('the data is not an execution state: its type is not the execution state\'s');
|
|
516
586
|
}
|
|
517
587
|
|
|
588
|
+
/**
|
|
589
|
+
* A dataflow run's state but its graph, its tasks, its reactive tracking and
|
|
590
|
+
* its events: what a poll of the run serves but the events, and the sequence
|
|
591
|
+
* number of its last event, past which a poll reads the events it has not
|
|
592
|
+
* served.
|
|
593
|
+
*
|
|
594
|
+
* @remarks
|
|
595
|
+
* An execution state store answers it for a workspace's latest run
|
|
596
|
+
* (`ExecutionStateStore.readLatestSummary`), so a poll reads it rather than
|
|
597
|
+
* the whole state, which grows with the dataflow and with every event of the
|
|
598
|
+
* run. A store may keep it beside the state, written with each change of the
|
|
599
|
+
* state; one that reads the whole state anyway derives it
|
|
600
|
+
* ({@link executionStateSummary}).
|
|
601
|
+
*/
|
|
602
|
+
export const ExecutionStateSummaryType = StructType({
|
|
603
|
+
/** The run's id, a UUIDv7 */
|
|
604
|
+
id: StringType,
|
|
605
|
+
/** When the run started */
|
|
606
|
+
startedAt: DateTimeType,
|
|
607
|
+
/** Number of tasks executed (not from cache) */
|
|
608
|
+
executed: IntegerType,
|
|
609
|
+
/** Number of tasks served from cache */
|
|
610
|
+
cached: IntegerType,
|
|
611
|
+
/** Number of tasks that failed */
|
|
612
|
+
failed: IntegerType,
|
|
613
|
+
/** Number of tasks skipped due to upstream failure */
|
|
614
|
+
skipped: IntegerType,
|
|
615
|
+
/** The run's status ({@link DataflowExecutionStatus}) */
|
|
616
|
+
status: StringType,
|
|
617
|
+
/** When the run ended */
|
|
618
|
+
completedAt: OptionType(DateTimeType),
|
|
619
|
+
/** Why the run failed, when it did */
|
|
620
|
+
error: OptionType(StringType),
|
|
621
|
+
/** The sequence number of the run's last event: 0 while it has none */
|
|
622
|
+
lastSeq: IntegerType,
|
|
623
|
+
});
|
|
624
|
+
export type ExecutionStateSummary = ValueTypeOf<typeof ExecutionStateSummaryType>;
|
|
625
|
+
|
|
626
|
+
/** Whether one event sequence number comes before another. */
|
|
627
|
+
const seqBefore = lessFor(IntegerType);
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The sequence number of a run's last event, as its state counts it: the
|
|
631
|
+
* state's own count, or the last event it holds, which a write has not yet
|
|
632
|
+
* handed its store, when that is later.
|
|
633
|
+
*
|
|
634
|
+
* @param state - The run's state
|
|
635
|
+
* @returns The sequence number: 0 while the run has no event
|
|
636
|
+
*/
|
|
637
|
+
export function lastEventSeq(state: Pick<DataflowExecutionState, 'events' | 'eventSeq'>): bigint {
|
|
638
|
+
const held = state.events.at(-1)?.value.seq;
|
|
639
|
+
return held !== undefined && seqBefore(state.eventSeq, held) ? held : state.eventSeq;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* The summary of a run's state ({@link ExecutionStateSummaryType}).
|
|
644
|
+
*
|
|
645
|
+
* @param state - The run's state
|
|
646
|
+
* @returns Its summary: its last event's sequence number is the last event's
|
|
647
|
+
* own ({@link lastEventSeq}), however the event was recorded
|
|
648
|
+
*/
|
|
649
|
+
export function executionStateSummary(state: DataflowExecutionState): ExecutionStateSummary {
|
|
650
|
+
return {
|
|
651
|
+
id: state.id,
|
|
652
|
+
startedAt: state.startedAt,
|
|
653
|
+
executed: state.executed,
|
|
654
|
+
cached: state.cached,
|
|
655
|
+
failed: state.failed,
|
|
656
|
+
skipped: state.skipped,
|
|
657
|
+
status: state.status,
|
|
658
|
+
completedAt: state.completedAt,
|
|
659
|
+
error: state.error,
|
|
660
|
+
lastSeq: lastEventSeq(state),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
518
664
|
// =============================================================================
|
|
519
665
|
// Dataflow Run History
|
|
520
666
|
// =============================================================================
|
package/src/dataset-blob.spec.ts
CHANGED
|
@@ -29,6 +29,54 @@ import { encodeDatasetBlob, writeCollectionManifest, type CollectionSegmentRef }
|
|
|
29
29
|
|
|
30
30
|
const TableType = DictType(StringType, IntegerType);
|
|
31
31
|
|
|
32
|
+
/** `n` rows, in canonical order: 20,000 span sixteen segments. */
|
|
33
|
+
function rowsOf(n: number): SortedMap<string, bigint> {
|
|
34
|
+
return new SortedMap(
|
|
35
|
+
Array.from({ length: n }, (_, i): [string, bigint] => [`k${String(i).padStart(6, '0')}`, BigInt(i)]),
|
|
36
|
+
compareFor(StringType));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A write the test ends, as the store would: stored, or failed. */
|
|
40
|
+
interface HeldWrite {
|
|
41
|
+
hash: string;
|
|
42
|
+
end: (failure?: Error) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A sink whose writes are held until the test ends each one: the writes in
|
|
46
|
+
* flight, and the hash of every object it was handed, in order. */
|
|
47
|
+
function heldSink(): { sink: (bytes: Uint8Array) => Promise<string>; open: HeldWrite[]; calls: string[] } {
|
|
48
|
+
const open: HeldWrite[] = [];
|
|
49
|
+
const calls: string[] = [];
|
|
50
|
+
const sink = (bytes: Uint8Array): Promise<string> => new Promise((resolve, reject) => {
|
|
51
|
+
const hash = sha256Hex(bytes);
|
|
52
|
+
calls.push(hash);
|
|
53
|
+
const write: HeldWrite = {
|
|
54
|
+
hash,
|
|
55
|
+
end: (failure) => {
|
|
56
|
+
open.splice(open.indexOf(write), 1);
|
|
57
|
+
if (failure === undefined) resolve(hash);
|
|
58
|
+
else reject(failure);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
open.push(write);
|
|
62
|
+
});
|
|
63
|
+
return { sink, open, calls };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lets `turns` rounds of the event loop pass. */
|
|
67
|
+
async function turns(turns: number): Promise<void> {
|
|
68
|
+
for (let i = 0; i < turns; i++) await new Promise((resolve) => setImmediate(resolve));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Lets the event loop run until `done` holds. */
|
|
72
|
+
async function until(done: () => boolean, what: string): Promise<void> {
|
|
73
|
+
for (let i = 0; i < 10_000; i++) {
|
|
74
|
+
if (done()) return;
|
|
75
|
+
await turns(1);
|
|
76
|
+
}
|
|
77
|
+
assert.fail(`never: ${what}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
32
80
|
describe('writeCollectionManifest', () => {
|
|
33
81
|
it("writes the manifest writer's manifest for the value, byte for byte", async () => {
|
|
34
82
|
const value = new SortedMap(
|
|
@@ -96,6 +144,105 @@ describe('writeCollectionManifest', () => {
|
|
|
96
144
|
assert.deepEqual(reads, [], 'the seam is decided by the fence that followed');
|
|
97
145
|
for (const entry of entries.slice(0, half)) assert.ok(!written.has(entry.hash), `segment ${entry.hash.slice(0, 8)} is carried`);
|
|
98
146
|
});
|
|
147
|
+
|
|
148
|
+
it('has the sink write the objects at once, as many as it is given, in order, and returns once every one is stored', async () => {
|
|
149
|
+
const value = rowsOf(20_000);
|
|
150
|
+
const expected = await writeCollectionManifest(TableType, [{ elements: value.entries() }], async (bytes) => sha256Hex(bytes));
|
|
151
|
+
const entries = decodeCollectionManifest(expected).entries;
|
|
152
|
+
const objects = entries.length + 1;
|
|
153
|
+
const { sink, open, calls } = heldSink();
|
|
154
|
+
let manifest: Uint8Array | undefined;
|
|
155
|
+
const writing = writeCollectionManifest(TableType, [{ elements: value.entries() }], sink, { inFlight: 3 })
|
|
156
|
+
.then((bytes) => {
|
|
157
|
+
manifest = bytes;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Each write ends only when the test ends it, the last begun first, so the
|
|
161
|
+
// writes end in another order than they began. Between ends, the writer
|
|
162
|
+
// has as many in flight as it may: three, or the objects left.
|
|
163
|
+
while (manifest === undefined) {
|
|
164
|
+
const inFlight = (): number => Math.min(3, objects - (calls.length - open.length));
|
|
165
|
+
await until(() => manifest !== undefined || open.length === inFlight(), 'as many writes in flight as may be');
|
|
166
|
+
await turns(20);
|
|
167
|
+
if (manifest !== undefined) break;
|
|
168
|
+
assert.equal(open.length, inFlight(), `${open.length} writes in flight, of ${calls.length} begun`);
|
|
169
|
+
open.at(-1)!.end();
|
|
170
|
+
}
|
|
171
|
+
await writing;
|
|
172
|
+
|
|
173
|
+
assert.deepEqual(manifest, expected, 'the manifest a write at a time writes, each entry in its place');
|
|
174
|
+
assert.deepEqual(calls, [...entries.map((entry) => entry.hash), decodeCollectionManifest(expected).header],
|
|
175
|
+
'the segments, in order, and then the header');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** A writer's failure, once it has failed: read through a function, which
|
|
179
|
+
* no assertion narrows. */
|
|
180
|
+
function failureOf(writing: Promise<unknown>): () => { failure: unknown } | undefined {
|
|
181
|
+
let settled: { failure: unknown } | undefined;
|
|
182
|
+
writing.then(() => {
|
|
183
|
+
settled = { failure: new Error('the writer did not fail') };
|
|
184
|
+
}, (failure: unknown) => {
|
|
185
|
+
settled = { failure };
|
|
186
|
+
});
|
|
187
|
+
return () => settled;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
it('starts no write after one fails, and throws its failure once the writes in flight have ended', async () => {
|
|
191
|
+
const { sink, open, calls } = heldSink();
|
|
192
|
+
const writing = writeCollectionManifest(TableType, [{ elements: rowsOf(20_000).entries() }], sink, { inFlight: 3 });
|
|
193
|
+
const settled = failureOf(writing);
|
|
194
|
+
await until(() => open.length === 3, 'three writes in flight');
|
|
195
|
+
const failure = new Error('the store is gone');
|
|
196
|
+
open[1]!.end(failure);
|
|
197
|
+
await turns(20);
|
|
198
|
+
assert.equal(calls.length, 3, 'nothing is begun after a write fails');
|
|
199
|
+
assert.equal(settled(), undefined, 'the writer waits for the writes still in flight');
|
|
200
|
+
|
|
201
|
+
for (const write of [...open]) write.end();
|
|
202
|
+
await writing.catch(() => {});
|
|
203
|
+
assert.equal(settled()?.failure, failure);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('throws a failure among the last writes, which no write after it meets', async () => {
|
|
207
|
+
const value = rowsOf(20_000);
|
|
208
|
+
const { header } = decodeCollectionManifest(
|
|
209
|
+
await writeCollectionManifest(TableType, [{ elements: value.entries() }], async (bytes) => sha256Hex(bytes)));
|
|
210
|
+
const { sink, open } = heldSink();
|
|
211
|
+
const writing = writeCollectionManifest(TableType, [{ elements: value.entries() }], sink, { inFlight: 3 });
|
|
212
|
+
const settled = failureOf(writing);
|
|
213
|
+
// Every object is stored but the header, the last begun.
|
|
214
|
+
const failure = new Error('the store is gone');
|
|
215
|
+
for (;;) {
|
|
216
|
+
await until(() => settled() !== undefined || open.length > 0, 'a write in flight, or the writer\'s end');
|
|
217
|
+
if (settled() !== undefined) break;
|
|
218
|
+
const write = open[0]!;
|
|
219
|
+
write.end(write.hash === header ? failure : undefined);
|
|
220
|
+
}
|
|
221
|
+
assert.equal(settled()?.failure, failure);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('throws a piece\'s failure once the writes in flight have ended', async () => {
|
|
225
|
+
const { sink, open } = heldSink();
|
|
226
|
+
// Rows in order, then one out of it.
|
|
227
|
+
let disordered = false;
|
|
228
|
+
function* rows(): Generator<[string, bigint]> {
|
|
229
|
+
yield* rowsOf(20_000).entries();
|
|
230
|
+
disordered = true;
|
|
231
|
+
yield ['a', 0n];
|
|
232
|
+
}
|
|
233
|
+
const writing = writeCollectionManifest(TableType, [{ elements: rows() }], sink, { inFlight: 64 });
|
|
234
|
+
const settled = failureOf(writing);
|
|
235
|
+
await until(() => disordered, 'the row out of order');
|
|
236
|
+
await turns(20);
|
|
237
|
+
assert.ok(open.length > 0, 'segments are in flight');
|
|
238
|
+
assert.equal(settled(), undefined, 'the writer waits for the writes still in flight');
|
|
239
|
+
|
|
240
|
+
for (const write of [...open]) write.end();
|
|
241
|
+
await writing.catch(() => {});
|
|
242
|
+
const failure = settled()?.failure;
|
|
243
|
+
assert.ok(failure instanceof Error, 'the piece\'s failure');
|
|
244
|
+
assert.match(failure.message, /ascend|order/);
|
|
245
|
+
});
|
|
99
246
|
});
|
|
100
247
|
|
|
101
248
|
describe('encodeDatasetBlob', () => {
|
package/src/dataset-blob.ts
CHANGED
|
@@ -59,6 +59,63 @@ import {
|
|
|
59
59
|
*/
|
|
60
60
|
export type SegmentSink = (bytes: Uint8Array) => Promise<string>;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* How many objects {@link writeCollectionManifest} has a sink write at once,
|
|
64
|
+
* unless its caller says otherwise: a store whose writes are round trips is
|
|
65
|
+
* waited on once for this many segments, rather than once for each.
|
|
66
|
+
*/
|
|
67
|
+
export const SEGMENT_WRITES_IN_FLIGHT = 16;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The writes of one manifest's objects: each started in the order it is
|
|
71
|
+
* handed over, at most `width` in flight, and none left running once the
|
|
72
|
+
* manifest's writer returns.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
class SinkWrites {
|
|
77
|
+
private readonly inFlight = new Set<Promise<void>>();
|
|
78
|
+
private failure: { error: unknown } | undefined;
|
|
79
|
+
|
|
80
|
+
constructor(private readonly sink: SegmentSink, private readonly width: number) {}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Starts writing `bytes` once fewer than `width` writes are in flight.
|
|
84
|
+
*
|
|
85
|
+
* @param bytes - The object
|
|
86
|
+
* @param named - Told the object's hash once it is stored
|
|
87
|
+
* @throws The first failed write's error, once one has failed: nothing more
|
|
88
|
+
* is started after it.
|
|
89
|
+
*/
|
|
90
|
+
async start(bytes: Uint8Array, named: (hash: string) => void): Promise<void> {
|
|
91
|
+
while (this.failure === undefined && this.inFlight.size >= this.width) await Promise.race(this.inFlight);
|
|
92
|
+
if (this.failure !== undefined) throw this.failure.error;
|
|
93
|
+
const write: Promise<void> = this.sink(bytes)
|
|
94
|
+
.then(named, (error: unknown) => {
|
|
95
|
+
this.failure ??= { error };
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
this.inFlight.delete(write);
|
|
99
|
+
});
|
|
100
|
+
this.inFlight.add(write);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Waits for every write started, whatever its outcome. */
|
|
104
|
+
async settled(): Promise<void> {
|
|
105
|
+
await Promise.all(this.inFlight);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Waits for every write started.
|
|
110
|
+
*
|
|
111
|
+
* @throws The first failed write's error.
|
|
112
|
+
*/
|
|
113
|
+
async stored(): Promise<void> {
|
|
114
|
+
await this.settled();
|
|
115
|
+
if (this.failure !== undefined) throw this.failure.error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
62
119
|
/**
|
|
63
120
|
* Whether a dataset root type is a collection — the kinds stored segmented +
|
|
64
121
|
* indexed so the paged read API can seek.
|
|
@@ -106,49 +163,69 @@ export type CollectionPiece = Beast2RecutPiece<EastType, CollectionSegmentRef>;
|
|
|
106
163
|
* Writer's own: a piece's segments are carried over only when that is the
|
|
107
164
|
* header they are under, which is the caller's to establish.
|
|
108
165
|
*
|
|
166
|
+
* The objects are written at once, `inFlight` at a time — the segments in
|
|
167
|
+
* order, then the header — and the manifest is returned once every one is
|
|
168
|
+
* stored, so the caller stores the manifest after the objects it names. The
|
|
169
|
+
* sink is called in segment order, and each segment's blob is held until its
|
|
170
|
+
* write ends.
|
|
171
|
+
*
|
|
109
172
|
* @param type - The collection type (Array / Set / Dict)
|
|
110
173
|
* @param pieces - The collection's pieces, in order
|
|
111
174
|
* @param sink - Writes one object and returns its hash
|
|
175
|
+
* @param options - `inFlight`, the most objects the sink writes at once
|
|
176
|
+
* (default {@link SEGMENT_WRITES_IN_FLIGHT})
|
|
112
177
|
* @returns The manifest's bytes — the caller stores them, and its hash is the
|
|
113
178
|
* dataset's content address
|
|
114
179
|
* @throws {Error} When a piece's elements do not ascend (Set / Dict), a
|
|
115
180
|
* segment's blob is not the one segment its reference describes, or the sink
|
|
116
|
-
* rejects
|
|
181
|
+
* rejects: once the writes in flight have ended, so none runs on after the
|
|
182
|
+
* writer returns.
|
|
117
183
|
*/
|
|
118
184
|
export async function writeCollectionManifest(
|
|
119
185
|
type: EastType | EastTypeValue,
|
|
120
186
|
pieces: Iterable<CollectionPiece> | AsyncIterable<CollectionPiece>,
|
|
121
187
|
sink: SegmentSink,
|
|
188
|
+
options: { inFlight?: number } = {},
|
|
122
189
|
): Promise<Uint8Array> {
|
|
123
190
|
const typeValue = asTypeValue(type);
|
|
124
191
|
const entries: CollectionManifestEntry[] = [];
|
|
192
|
+
const writes = new SinkWrites(sink, options.inFlight ?? SEGMENT_WRITES_IN_FLIGHT);
|
|
193
|
+
/** Writes a segment the manifest names at the place it takes now. */
|
|
194
|
+
const write = (blob: Uint8Array, fence: Uint8Array, count: number): Promise<void> => {
|
|
195
|
+
const at = entries.push({ hash: '', fence, count: BigInt(count), bytes: BigInt(blob.byteLength) }) - 1;
|
|
196
|
+
return writes.start(blob, (hash) => {
|
|
197
|
+
entries[at] = { ...entries[at]!, hash };
|
|
198
|
+
});
|
|
199
|
+
};
|
|
125
200
|
// Frames deflate on the worker pool once the value is large enough to be
|
|
126
201
|
// worth it; the pool holds the frames in flight and nothing more.
|
|
127
202
|
const recut = recutBeast2For<EastType, CollectionSegmentRef>(typeValue, { parallel: true });
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
203
|
+
let header = '';
|
|
204
|
+
try {
|
|
205
|
+
const stats = await recut(pieces, {
|
|
206
|
+
written: (segment) => write(segment.blob, segment.fence, segment.count),
|
|
207
|
+
carried: async (ref) => {
|
|
208
|
+
if (ref.entry !== undefined) {
|
|
209
|
+
entries.push(ref.entry);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await write(await ref.read(), ref.fence, ref.count);
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
await writes.start(stats.header, (hash) => {
|
|
216
|
+
header = hash;
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
await writes.settled();
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
await writes.stored();
|
|
146
223
|
return encodeCollectionManifest({
|
|
147
224
|
kind: COLLECTION_MANIFEST_KIND,
|
|
148
225
|
level: 0n,
|
|
149
226
|
type: typeValue,
|
|
150
227
|
rule: segmentRuleFor(typeValue),
|
|
151
|
-
header
|
|
228
|
+
header,
|
|
152
229
|
entries,
|
|
153
230
|
});
|
|
154
231
|
}
|