@outputai/core 0.12.0 → 0.12.1-next.1243f78.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/package.json +1 -1
- package/src/helpers/function.js +0 -16
- package/src/helpers/function.spec.js +1 -47
- package/src/helpers/promise.js +8 -26
- package/src/helpers/promise.spec.js +49 -22
- package/src/worker/catalog_workflow/catalog_publisher.js +149 -0
- package/src/worker/catalog_workflow/catalog_publisher.spec.js +317 -0
- package/src/worker/configs.js +4 -4
- package/src/worker/configs.spec.js +5 -5
- package/src/worker/connection_monitor.js +83 -55
- package/src/worker/connection_monitor.spec.js +258 -112
- package/src/worker/index.js +86 -144
- package/src/worker/index.spec.js +339 -253
- package/src/worker/interruption.js +57 -18
- package/src/worker/interruption.spec.js +139 -60
- package/src/worker/shutdown.js +53 -0
- package/src/worker/shutdown.spec.js +197 -0
- package/src/worker/worker_runner.js +66 -0
- package/src/worker/worker_runner.spec.js +220 -0
- package/src/worker/catalog_workflow/catalog_job.js +0 -152
- package/src/worker/catalog_workflow/catalog_job.spec.js +0 -218
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { WorkerRunner } from './worker_runner.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stands in for a Temporal worker: run() only settles once the drain finishes,
|
|
6
|
+
* and shutdown() moves the run state out of RUNNING the way the SDK does.
|
|
7
|
+
*/
|
|
8
|
+
const createWorker = () => {
|
|
9
|
+
const state = { runState: 'INITIALIZED', resolve: null, reject: null };
|
|
10
|
+
|
|
11
|
+
const worker = {
|
|
12
|
+
run: vi.fn( () => {
|
|
13
|
+
state.runState = 'RUNNING';
|
|
14
|
+
return new Promise( ( resolve, reject ) => {
|
|
15
|
+
state.resolve = resolve;
|
|
16
|
+
state.reject = reject;
|
|
17
|
+
} );
|
|
18
|
+
} ),
|
|
19
|
+
shutdown: vi.fn( () => {
|
|
20
|
+
state.runState = 'STOPPING';
|
|
21
|
+
} ),
|
|
22
|
+
getStatus: vi.fn( () => ( { runState: state.runState } ) )
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
worker,
|
|
27
|
+
drain: () => {
|
|
28
|
+
state.runState = 'STOPPED';
|
|
29
|
+
state.resolve();
|
|
30
|
+
},
|
|
31
|
+
fail: error => {
|
|
32
|
+
state.runState = 'STOPPED';
|
|
33
|
+
state.reject( error );
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const createRunner = ( worker, signal = new AbortController().signal ) => new WorkerRunner( { worker, signal } );
|
|
39
|
+
|
|
40
|
+
describe( 'WorkerRunner', () => {
|
|
41
|
+
it( 'polls the task queue and reports running until the worker settles', async () => {
|
|
42
|
+
const { worker, drain } = createWorker();
|
|
43
|
+
const runner = createRunner( worker );
|
|
44
|
+
|
|
45
|
+
const run = runner.start();
|
|
46
|
+
|
|
47
|
+
expect( worker.run ).toHaveBeenCalledOnce();
|
|
48
|
+
expect( runner.running ).toBe( true );
|
|
49
|
+
|
|
50
|
+
drain();
|
|
51
|
+
await run;
|
|
52
|
+
|
|
53
|
+
expect( runner.running ).toBe( false );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'returns the same execution when started more than once', async () => {
|
|
57
|
+
const { worker, drain } = createWorker();
|
|
58
|
+
const runner = createRunner( worker );
|
|
59
|
+
|
|
60
|
+
const first = runner.start();
|
|
61
|
+
const second = runner.start();
|
|
62
|
+
|
|
63
|
+
expect( second ).toBe( first );
|
|
64
|
+
expect( worker.run ).toHaveBeenCalledOnce();
|
|
65
|
+
|
|
66
|
+
drain();
|
|
67
|
+
await first;
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
it( 'never polls when the signal is already aborted', async () => {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
controller.abort();
|
|
73
|
+
const { worker } = createWorker();
|
|
74
|
+
const runner = createRunner( worker, controller.signal );
|
|
75
|
+
|
|
76
|
+
await expect( runner.start() ).resolves.toBeUndefined();
|
|
77
|
+
|
|
78
|
+
expect( worker.run ).not.toHaveBeenCalled();
|
|
79
|
+
expect( worker.shutdown ).not.toHaveBeenCalled();
|
|
80
|
+
expect( runner.running ).toBe( false );
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
it( 'drains the worker when the signal aborts', async () => {
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const { worker, drain } = createWorker();
|
|
86
|
+
const runner = createRunner( worker, controller.signal );
|
|
87
|
+
|
|
88
|
+
const run = runner.start();
|
|
89
|
+
controller.abort();
|
|
90
|
+
|
|
91
|
+
expect( worker.shutdown ).toHaveBeenCalledOnce();
|
|
92
|
+
expect( runner.running ).toBe( true );
|
|
93
|
+
|
|
94
|
+
drain();
|
|
95
|
+
await run;
|
|
96
|
+
|
|
97
|
+
expect( runner.running ).toBe( false );
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
it( 'drains on stop and resolves once the worker settles', async () => {
|
|
101
|
+
const { worker, drain } = createWorker();
|
|
102
|
+
const runner = createRunner( worker );
|
|
103
|
+
|
|
104
|
+
const run = runner.start();
|
|
105
|
+
const stopped = runner.stop();
|
|
106
|
+
|
|
107
|
+
expect( worker.shutdown ).toHaveBeenCalledOnce();
|
|
108
|
+
expect( runner.running ).toBe( true );
|
|
109
|
+
|
|
110
|
+
drain();
|
|
111
|
+
|
|
112
|
+
await expect( stopped ).resolves.toBeUndefined();
|
|
113
|
+
await run;
|
|
114
|
+
|
|
115
|
+
expect( runner.running ).toBe( false );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'does not shut down a worker that is not running', async () => {
|
|
119
|
+
const { worker } = createWorker();
|
|
120
|
+
const runner = createRunner( worker );
|
|
121
|
+
|
|
122
|
+
await expect( runner.stop() ).resolves.toBeUndefined();
|
|
123
|
+
|
|
124
|
+
expect( worker.run ).not.toHaveBeenCalled();
|
|
125
|
+
expect( worker.shutdown ).not.toHaveBeenCalled();
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
it( 'shuts the worker down once when stopped twice', async () => {
|
|
129
|
+
const { worker, drain } = createWorker();
|
|
130
|
+
const runner = createRunner( worker );
|
|
131
|
+
|
|
132
|
+
const run = runner.start();
|
|
133
|
+
runner.stop();
|
|
134
|
+
runner.stop();
|
|
135
|
+
|
|
136
|
+
expect( worker.shutdown ).toHaveBeenCalledOnce();
|
|
137
|
+
|
|
138
|
+
drain();
|
|
139
|
+
await run;
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
it( 'shuts the worker down once when aborted and then stopped', async () => {
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const { worker, drain } = createWorker();
|
|
145
|
+
const runner = createRunner( worker, controller.signal );
|
|
146
|
+
|
|
147
|
+
const run = runner.start();
|
|
148
|
+
controller.abort();
|
|
149
|
+
const stopped = runner.stop();
|
|
150
|
+
|
|
151
|
+
expect( worker.shutdown ).toHaveBeenCalledOnce();
|
|
152
|
+
|
|
153
|
+
drain();
|
|
154
|
+
|
|
155
|
+
await expect( stopped ).resolves.toBeUndefined();
|
|
156
|
+
await run;
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
it( 'rejects when the worker fails', async () => {
|
|
160
|
+
const error = new Error( 'poll failed' );
|
|
161
|
+
const { worker, fail } = createWorker();
|
|
162
|
+
const runner = createRunner( worker );
|
|
163
|
+
|
|
164
|
+
const run = runner.start();
|
|
165
|
+
const rejection = expect( run ).rejects.toBe( error );
|
|
166
|
+
|
|
167
|
+
fail( error );
|
|
168
|
+
await rejection;
|
|
169
|
+
|
|
170
|
+
expect( runner.running ).toBe( false );
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
it( 'reports the failure through stop so a drain error is never silent', async () => {
|
|
174
|
+
const error = new Error( 'poll failed' );
|
|
175
|
+
const { worker, fail } = createWorker();
|
|
176
|
+
const runner = createRunner( worker );
|
|
177
|
+
|
|
178
|
+
const run = runner.start();
|
|
179
|
+
const rejection = expect( run ).rejects.toBe( error );
|
|
180
|
+
|
|
181
|
+
fail( error );
|
|
182
|
+
await rejection;
|
|
183
|
+
|
|
184
|
+
await expect( runner.stop() ).rejects.toBe( error );
|
|
185
|
+
expect( worker.shutdown ).not.toHaveBeenCalled();
|
|
186
|
+
} );
|
|
187
|
+
|
|
188
|
+
it( 'never rethrows a drain that fails after the signal aborted', async () => {
|
|
189
|
+
const error = new Error( 'drain failed' );
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
const { worker, fail } = createWorker();
|
|
192
|
+
const runner = createRunner( worker, controller.signal );
|
|
193
|
+
|
|
194
|
+
const run = runner.start();
|
|
195
|
+
const rejection = expect( run ).rejects.toBe( error );
|
|
196
|
+
|
|
197
|
+
// Node rethrows a rejected promise returned by an event listener, so the abort listener must not return one
|
|
198
|
+
controller.abort();
|
|
199
|
+
fail( error );
|
|
200
|
+
|
|
201
|
+
await rejection;
|
|
202
|
+
|
|
203
|
+
expect( runner.running ).toBe( false );
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'reports a drain that fails after stop was called', async () => {
|
|
207
|
+
const error = new Error( 'drain failed' );
|
|
208
|
+
const { worker, fail } = createWorker();
|
|
209
|
+
const runner = createRunner( worker );
|
|
210
|
+
|
|
211
|
+
const run = runner.start();
|
|
212
|
+
const rejection = expect( run ).rejects.toBe( error );
|
|
213
|
+
const stopped = runner.stop();
|
|
214
|
+
|
|
215
|
+
fail( error );
|
|
216
|
+
|
|
217
|
+
await expect( stopped ).rejects.toBe( error );
|
|
218
|
+
await rejection;
|
|
219
|
+
} );
|
|
220
|
+
} );
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { Client, WorkflowNotFoundError } from '@temporalio/client';
|
|
2
|
-
import { WorkflowExecutionAlreadyStartedError, WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
3
|
-
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
|
-
import { catalogId, taskQueue } from '../configs.js';
|
|
5
|
-
import { createChildLogger } from '#logger';
|
|
6
|
-
import { CancellablePromise } from '#helpers/promise';
|
|
7
|
-
|
|
8
|
-
const log = createChildLogger( 'Catalog' );
|
|
9
|
-
|
|
10
|
-
class CancellationError extends Error {};
|
|
11
|
-
|
|
12
|
-
// Note, functions don't log on "WorkflowNotFound" errors,
|
|
13
|
-
// because they happen when the catalog is not running at all.
|
|
14
|
-
|
|
15
|
-
/** Make sure the latest version of the catalog workflow is running. Stateful. */
|
|
16
|
-
export class CatalogJob {
|
|
17
|
-
#cancellation = new CancellablePromise();
|
|
18
|
-
#connection = null;
|
|
19
|
-
#namespace = null;
|
|
20
|
-
#catalog = null;
|
|
21
|
-
#catalogHash = null;
|
|
22
|
-
|
|
23
|
-
#running = false;
|
|
24
|
-
#executePromise = null;
|
|
25
|
-
#onErrorCb = null;
|
|
26
|
-
#error = null;
|
|
27
|
-
|
|
28
|
-
#runCancellable = promise => Promise
|
|
29
|
-
.race( [ promise, this.#cancellation.promise.then( () => {
|
|
30
|
-
throw new CancellationError();
|
|
31
|
-
} ) ] );
|
|
32
|
-
|
|
33
|
-
/** Check if the currently running catalog has the same hash as instance. */
|
|
34
|
-
async #checkCatalogIsTheSame( handle ) {
|
|
35
|
-
log.info( 'Checking running catalog hash against worker hash...' );
|
|
36
|
-
return this.#runCancellable( handle.describe() ).then( d => d.memo?.hash === this.#catalogHash ).catch( e => {
|
|
37
|
-
if ( e instanceof CancellationError ) {
|
|
38
|
-
throw e;
|
|
39
|
-
}
|
|
40
|
-
if ( !( e instanceof WorkflowNotFoundError ) ) {
|
|
41
|
-
log.warn( 'Error describing catalog workflow', { error: e.message } );
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
} );
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** Check if the catalog workflow is running. */
|
|
48
|
-
async #checkCatalogRunning( handle ) {
|
|
49
|
-
log.info( 'Checking if the catalog workflow is running...' );
|
|
50
|
-
return this.#runCancellable( handle.describe() ).then( d => !d.closeTime ).catch( e => {
|
|
51
|
-
if ( e instanceof CancellationError ) {
|
|
52
|
-
throw e;
|
|
53
|
-
}
|
|
54
|
-
if ( !( e instanceof WorkflowNotFoundError ) ) {
|
|
55
|
-
log.warn( 'Error describing catalog workflow', { error: e.message } );
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
} );
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** Complete previous running catalog workflow. */
|
|
62
|
-
async #completePreviousCatalog( handle ) {
|
|
63
|
-
log.info( 'Completing previous catalog workflow...' );
|
|
64
|
-
return this.#runCancellable( handle.executeUpdate( 'complete', { args: [] } ) ).catch( e => {
|
|
65
|
-
if ( e instanceof CancellationError ) {
|
|
66
|
-
throw e;
|
|
67
|
-
}
|
|
68
|
-
if ( !( e instanceof WorkflowNotFoundError ) ) {
|
|
69
|
-
log.warn( 'Error completing previous catalog workflow', { error: e.message } );
|
|
70
|
-
}
|
|
71
|
-
} );
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
/** Run the sequence to start the catalog */
|
|
75
|
-
async #execute() {
|
|
76
|
-
const client = new Client( { connection: this.#connection, namespace: this.#namespace } );
|
|
77
|
-
const handle = client.workflow.getHandle( catalogId );
|
|
78
|
-
|
|
79
|
-
if ( await this.#checkCatalogRunning( handle ) ) {
|
|
80
|
-
if ( await this.#checkCatalogIsTheSame( handle ) ) {
|
|
81
|
-
log.info( 'Current catalog workflow hash matches worker, restart skipped' );
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
await this.#completePreviousCatalog( handle );
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const startArguments = {
|
|
88
|
-
taskQueue,
|
|
89
|
-
workflowId: catalogId,
|
|
90
|
-
workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL,
|
|
91
|
-
args: [ this.#catalog ],
|
|
92
|
-
memo: {
|
|
93
|
-
workflowNames: this.#catalog.workflowNames,
|
|
94
|
-
hash: this.#catalogHash
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
log.info( 'Starting catalog workflow...' );
|
|
99
|
-
try {
|
|
100
|
-
await this.#runCancellable( client.workflow.start( WORKFLOW_CATALOG, startArguments ) );
|
|
101
|
-
} catch ( error ) {
|
|
102
|
-
// if the error was caused by the catalog existing and its hash is the same as the one from the worker, just ignore the error
|
|
103
|
-
if ( error instanceof WorkflowExecutionAlreadyStartedError && await this.#checkCatalogIsTheSame( handle ) ) {
|
|
104
|
-
log.info( 'Ignoring start error: it failed because execution already started but catalog hash matches worker' );
|
|
105
|
-
} else {
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
log.info( 'Startup completed' );
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
constructor( { connection, namespace, catalog, catalogHash } ) {
|
|
113
|
-
this.#connection = connection;
|
|
114
|
-
this.#namespace = namespace;
|
|
115
|
-
this.#catalog = catalog;
|
|
116
|
-
this.#catalogHash = catalogHash;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
onError( cb ) {
|
|
120
|
-
this.#onErrorCb = cb;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
get running() {
|
|
124
|
-
return this.#running;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
interrupt() {
|
|
128
|
-
if ( this.#running ) {
|
|
129
|
-
this.#cancellation.complete();
|
|
130
|
-
}
|
|
131
|
-
return this.#executePromise ?? Promise.resolve();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
get error() {
|
|
135
|
-
return this.#error;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async run() {
|
|
139
|
-
this.#running = true;
|
|
140
|
-
this.#executePromise = this.#execute()
|
|
141
|
-
.catch( error => {
|
|
142
|
-
if ( !( error instanceof CancellationError ) ) {
|
|
143
|
-
this.#error = error;
|
|
144
|
-
this.#onErrorCb?.( error );
|
|
145
|
-
}
|
|
146
|
-
} )
|
|
147
|
-
.finally( () => {
|
|
148
|
-
this.#running = false;
|
|
149
|
-
} );
|
|
150
|
-
return this.#executePromise;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { WorkflowNotFoundError } from '@temporalio/client';
|
|
3
|
-
import { WorkflowExecutionAlreadyStartedError, WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
4
|
-
import { CatalogJob } from './catalog_job.js';
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
catalogId,
|
|
8
|
-
describeMock,
|
|
9
|
-
executeUpdateMock,
|
|
10
|
-
mockLog,
|
|
11
|
-
taskQueue,
|
|
12
|
-
workflowStartMock
|
|
13
|
-
} = vi.hoisted( () => ( {
|
|
14
|
-
catalogId: 'test-catalog',
|
|
15
|
-
describeMock: vi.fn(),
|
|
16
|
-
executeUpdateMock: vi.fn(),
|
|
17
|
-
mockLog: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
18
|
-
taskQueue: 'test-queue',
|
|
19
|
-
workflowStartMock: vi.fn()
|
|
20
|
-
} ) );
|
|
21
|
-
|
|
22
|
-
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
23
|
-
vi.mock( '#consts', () => ( { WORKFLOW_CATALOG: 'catalog' } ) );
|
|
24
|
-
vi.mock( '../configs.js', () => ( { catalogId, taskQueue } ) );
|
|
25
|
-
vi.mock( '@temporalio/client', async importOriginal => {
|
|
26
|
-
const actual = await importOriginal();
|
|
27
|
-
return {
|
|
28
|
-
...actual,
|
|
29
|
-
Client: vi.fn().mockImplementation( function () {
|
|
30
|
-
return {
|
|
31
|
-
workflow: {
|
|
32
|
-
start: workflowStartMock,
|
|
33
|
-
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
} )
|
|
37
|
-
};
|
|
38
|
-
} );
|
|
39
|
-
|
|
40
|
-
const mockConnection = {};
|
|
41
|
-
const namespace = 'default';
|
|
42
|
-
const catalog = { workflows: [], workflowNames: { workflow: 'workflow', alias: 'workflow' }, activities: {} };
|
|
43
|
-
const catalogHash = 'catalog-hash';
|
|
44
|
-
const startArguments = {
|
|
45
|
-
taskQueue,
|
|
46
|
-
workflowId: catalogId,
|
|
47
|
-
workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL,
|
|
48
|
-
args: [ catalog ],
|
|
49
|
-
memo: {
|
|
50
|
-
workflowNames: catalog.workflowNames,
|
|
51
|
-
hash: catalogHash
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const createJob = () => new CatalogJob( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
56
|
-
|
|
57
|
-
const flushPromises = async () => Array
|
|
58
|
-
.from( { length: 10 } )
|
|
59
|
-
.reduce( promise => promise.then( () => Promise.resolve() ), Promise.resolve() );
|
|
60
|
-
|
|
61
|
-
describe( 'CatalogJob', () => {
|
|
62
|
-
beforeEach( () => {
|
|
63
|
-
vi.clearAllMocks();
|
|
64
|
-
describeMock.mockResolvedValue( { closeTime: '2024-01-01T00:00:00Z' } );
|
|
65
|
-
executeUpdateMock.mockResolvedValue( undefined );
|
|
66
|
-
workflowStartMock.mockResolvedValue( undefined );
|
|
67
|
-
} );
|
|
68
|
-
|
|
69
|
-
it( 'completes a previous running stale catalog before starting the new workflow', async () => {
|
|
70
|
-
describeMock.mockResolvedValue( { closeTime: undefined, memo: { hash: 'old-catalog-hash' } } );
|
|
71
|
-
const job = createJob();
|
|
72
|
-
|
|
73
|
-
await job.run();
|
|
74
|
-
|
|
75
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
76
|
-
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
77
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', startArguments );
|
|
78
|
-
expect( job.error ).toBeNull();
|
|
79
|
-
expect( job.running ).toBe( false );
|
|
80
|
-
} );
|
|
81
|
-
|
|
82
|
-
it( 'keeps the existing catalog workflow when the running hash matches', async () => {
|
|
83
|
-
describeMock.mockResolvedValue( { closeTime: undefined, memo: { hash: catalogHash } } );
|
|
84
|
-
const job = createJob();
|
|
85
|
-
|
|
86
|
-
await job.run();
|
|
87
|
-
|
|
88
|
-
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
89
|
-
expect( workflowStartMock ).not.toHaveBeenCalled();
|
|
90
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Current catalog workflow hash matches worker, restart skipped' );
|
|
91
|
-
expect( job.error ).toBeNull();
|
|
92
|
-
} );
|
|
93
|
-
|
|
94
|
-
it( 'starts the catalog workflow when no previous catalog exists', async () => {
|
|
95
|
-
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
96
|
-
const job = createJob();
|
|
97
|
-
|
|
98
|
-
await job.run();
|
|
99
|
-
|
|
100
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
101
|
-
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
102
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', startArguments );
|
|
103
|
-
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
104
|
-
} );
|
|
105
|
-
|
|
106
|
-
it( 'starts the catalog workflow when the previous catalog is closed', async () => {
|
|
107
|
-
const job = createJob();
|
|
108
|
-
|
|
109
|
-
await job.run();
|
|
110
|
-
|
|
111
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
112
|
-
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
113
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', startArguments );
|
|
114
|
-
} );
|
|
115
|
-
|
|
116
|
-
it( 'warns and continues when describing or completing the previous catalog fails', async () => {
|
|
117
|
-
const completeError = new Error( 'complete failed' );
|
|
118
|
-
describeMock.mockResolvedValue( { closeTime: undefined, memo: { hash: 'old-catalog-hash' } } );
|
|
119
|
-
executeUpdateMock.mockRejectedValue( completeError );
|
|
120
|
-
const job = createJob();
|
|
121
|
-
|
|
122
|
-
await job.run();
|
|
123
|
-
|
|
124
|
-
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error completing previous catalog workflow', { error: completeError.message } );
|
|
125
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', expect.any( Object ) );
|
|
126
|
-
expect( job.error ).toBeNull();
|
|
127
|
-
} );
|
|
128
|
-
|
|
129
|
-
it( 'ignores an already-started error when the running catalog hash matches', async () => {
|
|
130
|
-
const alreadyStartedError = new WorkflowExecutionAlreadyStartedError( 'already started', catalogId, 'catalog' );
|
|
131
|
-
describeMock
|
|
132
|
-
.mockRejectedValueOnce( new WorkflowNotFoundError( 'not found' ) )
|
|
133
|
-
.mockResolvedValueOnce( { closeTime: undefined, memo: { hash: catalogHash } } );
|
|
134
|
-
workflowStartMock.mockRejectedValue( alreadyStartedError );
|
|
135
|
-
const job = createJob();
|
|
136
|
-
|
|
137
|
-
await job.run();
|
|
138
|
-
|
|
139
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', expect.any( Object ) );
|
|
140
|
-
expect( mockLog.info ).toHaveBeenCalledWith(
|
|
141
|
-
'Ignoring start error: it failed because execution already started but catalog hash matches worker'
|
|
142
|
-
);
|
|
143
|
-
expect( job.error ).toBeNull();
|
|
144
|
-
} );
|
|
145
|
-
|
|
146
|
-
it( 'stores start errors and calls the error callback', async () => {
|
|
147
|
-
const error = new Error( 'start failed' );
|
|
148
|
-
const onError = vi.fn();
|
|
149
|
-
workflowStartMock.mockRejectedValue( error );
|
|
150
|
-
const job = createJob();
|
|
151
|
-
|
|
152
|
-
job.onError( onError );
|
|
153
|
-
await job.run();
|
|
154
|
-
|
|
155
|
-
expect( job.error ).toBe( error );
|
|
156
|
-
expect( onError ).toHaveBeenCalledWith( error );
|
|
157
|
-
expect( job.running ).toBe( false );
|
|
158
|
-
} );
|
|
159
|
-
|
|
160
|
-
it( 'stores stale already-started errors and calls the error callback', async () => {
|
|
161
|
-
const alreadyStartedError = new WorkflowExecutionAlreadyStartedError( 'already started', catalogId, 'catalog' );
|
|
162
|
-
const onError = vi.fn();
|
|
163
|
-
describeMock
|
|
164
|
-
.mockRejectedValueOnce( new WorkflowNotFoundError( 'not found' ) )
|
|
165
|
-
.mockResolvedValueOnce( { closeTime: undefined, memo: { hash: 'old-catalog-hash' } } );
|
|
166
|
-
workflowStartMock.mockRejectedValue( alreadyStartedError );
|
|
167
|
-
const job = createJob();
|
|
168
|
-
|
|
169
|
-
job.onError( onError );
|
|
170
|
-
await job.run();
|
|
171
|
-
|
|
172
|
-
expect( job.error ).toBe( alreadyStartedError );
|
|
173
|
-
expect( onError ).toHaveBeenCalledWith( alreadyStartedError );
|
|
174
|
-
expect( job.running ).toBe( false );
|
|
175
|
-
} );
|
|
176
|
-
|
|
177
|
-
it( 'reports running while a catalog call is pending', async () => {
|
|
178
|
-
describeMock.mockReturnValue( new Promise( () => {} ) );
|
|
179
|
-
const job = createJob();
|
|
180
|
-
|
|
181
|
-
const run = job.run();
|
|
182
|
-
await flushPromises();
|
|
183
|
-
|
|
184
|
-
expect( job.running ).toBe( true );
|
|
185
|
-
|
|
186
|
-
await job.interrupt();
|
|
187
|
-
await run;
|
|
188
|
-
|
|
189
|
-
expect( job.running ).toBe( false );
|
|
190
|
-
expect( job.error ).toBeNull();
|
|
191
|
-
} );
|
|
192
|
-
|
|
193
|
-
it( 'interrupts pending catalog calls without storing an error or calling the callback', async () => {
|
|
194
|
-
const onError = vi.fn();
|
|
195
|
-
describeMock.mockReturnValue( new Promise( () => {} ) );
|
|
196
|
-
const job = createJob();
|
|
197
|
-
|
|
198
|
-
job.onError( onError );
|
|
199
|
-
const run = job.run();
|
|
200
|
-
await flushPromises();
|
|
201
|
-
|
|
202
|
-
const interrupt = job.interrupt();
|
|
203
|
-
|
|
204
|
-
await expect( interrupt ).resolves.toBeUndefined();
|
|
205
|
-
await expect( run ).resolves.toBeUndefined();
|
|
206
|
-
expect( job.error ).toBeNull();
|
|
207
|
-
expect( onError ).not.toHaveBeenCalled();
|
|
208
|
-
expect( job.running ).toBe( false );
|
|
209
|
-
} );
|
|
210
|
-
|
|
211
|
-
it( 'returns a resolved promise when interrupted before running', async () => {
|
|
212
|
-
const job = createJob();
|
|
213
|
-
|
|
214
|
-
await expect( job.interrupt() ).resolves.toBeUndefined();
|
|
215
|
-
expect( job.running ).toBe( false );
|
|
216
|
-
expect( job.error ).toBeNull();
|
|
217
|
-
} );
|
|
218
|
-
} );
|