@outputai/core 0.5.3-next.69060d7.0 → 0.5.3-next.bdf47aa.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 +2 -1
- package/src/worker/catalog_workflow/workflow.js +4 -1
- package/src/worker/index.js +5 -1
- package/src/worker/index.spec.js +6 -1
- package/src/worker/interceptors.js +5 -1
- package/src/worker/loader_tools.js +25 -0
- package/src/worker/start_catalog.js +78 -18
- package/src/worker/start_catalog.spec.js +75 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.5.3-next.
|
|
3
|
+
"version": "0.5.3-next.bdf47aa.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@temporalio/worker": "1.17.0",
|
|
45
45
|
"@temporalio/workflow": "1.17.0",
|
|
46
46
|
"decimal.js": "10.6.0",
|
|
47
|
+
"folder-hash": "4.1.3",
|
|
47
48
|
"json-stream-stringify": "3.1.6",
|
|
48
49
|
"redis": "5.12.1",
|
|
49
50
|
"stacktrace-parser": "0.1.11",
|
|
@@ -7,12 +7,15 @@ import { defineQuery, setHandler, condition, defineUpdate } from '@temporalio/wo
|
|
|
7
7
|
*
|
|
8
8
|
* @param {object} catalog - The catalog information
|
|
9
9
|
*/
|
|
10
|
-
export default async function catalogWorkflow( catalog ) {
|
|
10
|
+
export default async function catalogWorkflow( catalog, catalogHash ) {
|
|
11
11
|
const state = { canEnd: false };
|
|
12
12
|
|
|
13
13
|
// Returns the catalog
|
|
14
14
|
setHandler( defineQuery( 'get' ), () => catalog );
|
|
15
15
|
|
|
16
|
+
// Returns the catalog hash
|
|
17
|
+
setHandler( defineQuery( 'get_hash' ), () => catalogHash );
|
|
18
|
+
|
|
16
19
|
// Politely respond to a ping
|
|
17
20
|
setHandler( defineQuery( 'ping' ), () => 'pong' );
|
|
18
21
|
|
package/src/worker/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { bootstrapFetchProxy } from './proxy.js';
|
|
|
13
13
|
import { messageBus } from '#bus';
|
|
14
14
|
import './log_hooks.js';
|
|
15
15
|
import { BusEventType } from '#consts';
|
|
16
|
+
import { hashSourceCode } from './loader_tools.js';
|
|
16
17
|
|
|
17
18
|
const log = createChildLogger( 'Worker' );
|
|
18
19
|
|
|
@@ -54,6 +55,9 @@ const callerDir = process.argv[2];
|
|
|
54
55
|
log.info( 'Creating workflows catalog...' );
|
|
55
56
|
const catalog = createCatalog( { workflows, activities } );
|
|
56
57
|
|
|
58
|
+
log.info( 'Computing catalog source code hash...' );
|
|
59
|
+
const catalogHash = await hashSourceCode( callerDir );
|
|
60
|
+
|
|
57
61
|
log.info( 'Connecting Temporal...' );
|
|
58
62
|
const proxy = grpcProxy ? { type: 'http-connect', targetHost: grpcProxy } : undefined;
|
|
59
63
|
if ( proxy ) {
|
|
@@ -81,7 +85,7 @@ const callerDir = process.argv[2];
|
|
|
81
85
|
registerShutdown( { worker, log } );
|
|
82
86
|
|
|
83
87
|
log.info( 'Running worker...' );
|
|
84
|
-
await Promise.all( [ worker.run(), startCatalog( { connection, namespace, catalog } ) ] );
|
|
88
|
+
await Promise.all( [ worker.run(), startCatalog( { connection, namespace, catalog, catalogHash } ) ] );
|
|
85
89
|
|
|
86
90
|
log.info( 'Closing connection...' );
|
|
87
91
|
await connection.close();
|
package/src/worker/index.spec.js
CHANGED
|
@@ -40,6 +40,9 @@ vi.mock( './loader.js', () => ( {
|
|
|
40
40
|
createWorkflowsEntryPoint: createWorkflowsEntryPointMock
|
|
41
41
|
} ) );
|
|
42
42
|
|
|
43
|
+
const hashSourceCodeMock = vi.fn().mockResolvedValue( 'catalog-hash' );
|
|
44
|
+
vi.mock( './loader_tools.js', () => ( { hashSourceCode: hashSourceCodeMock } ) );
|
|
45
|
+
|
|
43
46
|
vi.mock( './sinks.js', () => ( { sinks: {} } ) );
|
|
44
47
|
|
|
45
48
|
const createCatalogMock = vi.fn().mockReturnValue( { workflows: [], activities: {} } );
|
|
@@ -105,6 +108,7 @@ describe( 'worker/index', () => {
|
|
|
105
108
|
expect( createWorkflowsEntryPointMock ).toHaveBeenCalledWith( [] );
|
|
106
109
|
expect( initTracing ).toHaveBeenCalled();
|
|
107
110
|
expect( createCatalogMock ).toHaveBeenCalledWith( { workflows: [], activities: {} } );
|
|
111
|
+
expect( hashSourceCodeMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
108
112
|
expect( bootstrapFetchProxyMock ).toHaveBeenCalled();
|
|
109
113
|
expect( NativeConnection.connect ).toHaveBeenCalledWith( {
|
|
110
114
|
address: configValues.address,
|
|
@@ -128,7 +132,8 @@ describe( 'worker/index', () => {
|
|
|
128
132
|
expect( startCatalogMock ).toHaveBeenCalledWith( {
|
|
129
133
|
connection: mockConnection,
|
|
130
134
|
namespace: configValues.namespace,
|
|
131
|
-
catalog: { workflows: [], activities: {} }
|
|
135
|
+
catalog: { workflows: [], activities: {} },
|
|
136
|
+
catalogHash: 'catalog-hash'
|
|
132
137
|
} );
|
|
133
138
|
|
|
134
139
|
runState.resolve();
|
|
@@ -6,5 +6,9 @@ const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
|
6
6
|
|
|
7
7
|
export const initInterceptors = ( { activities, workflows, connection } ) => ( {
|
|
8
8
|
workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
|
|
9
|
-
|
|
9
|
+
activity: [
|
|
10
|
+
() => ( {
|
|
11
|
+
inbound: new ActivityExecutionInterceptor( { activities, workflows, connection } )
|
|
12
|
+
} )
|
|
13
|
+
]
|
|
10
14
|
} );
|
|
@@ -2,6 +2,7 @@ import { dirname, resolve, sep } from 'path';
|
|
|
2
2
|
import { pathToFileURL } from 'url';
|
|
3
3
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
4
|
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'fs';
|
|
5
|
+
import { hashElement } from 'folder-hash';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Returns the real path for symlink
|
|
@@ -302,3 +303,27 @@ export async function *importComponents( files ) {
|
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Creates a hash of all source code in a given dir
|
|
309
|
+
*
|
|
310
|
+
* @param {string} rootDir
|
|
311
|
+
* @returns {string} Hash value
|
|
312
|
+
*/
|
|
313
|
+
export const hashSourceCode = async rootDir => {
|
|
314
|
+
try {
|
|
315
|
+
const { hash } = await hashElement( rootDir, {
|
|
316
|
+
folders: {
|
|
317
|
+
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test' ],
|
|
318
|
+
ignoreRootName: true
|
|
319
|
+
},
|
|
320
|
+
files: {
|
|
321
|
+
include: [ '*.js', '*.cjs', '*.mjs', '*.ts', '*.yaml', '*.yml', '*.json', '*.prompt' ],
|
|
322
|
+
ignoreRootName: true
|
|
323
|
+
}
|
|
324
|
+
} );
|
|
325
|
+
return hash;
|
|
326
|
+
} catch ( error ) {
|
|
327
|
+
throw new Error( `Error calculating hash from "${error}": ${error.message}`, { cause: error } );
|
|
328
|
+
}
|
|
329
|
+
};
|
|
@@ -1,36 +1,96 @@
|
|
|
1
1
|
import { Client, WorkflowNotFoundError } from '@temporalio/client';
|
|
2
|
-
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
2
|
+
import { WorkflowExecutionAlreadyStartedError, WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
3
3
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
4
|
import { catalogId, taskQueue } from './configs.js';
|
|
5
5
|
import { createChildLogger } from '#logger';
|
|
6
6
|
|
|
7
7
|
const log = createChildLogger( 'Catalog' );
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const catalogWorkflowHandle = client.workflow.getHandle( catalogId );
|
|
9
|
+
// Note, functions don't log on "WorkflowNotFound" errors,
|
|
10
|
+
// because they happen when the catalog is not running at all.
|
|
12
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Check if the currently running catalog has the same hash as the passed argument.
|
|
14
|
+
* @param {import('@temporalio/client').WorkflowHandle} handle
|
|
15
|
+
* @param {string} hash
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
const checkCatalogIsTheSame = async ( handle, hash ) => {
|
|
13
19
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
log.info( 'Checking running catalog hash against worker hash....' );
|
|
21
|
+
const runningHash = await handle.query( 'get_hash' );
|
|
22
|
+
return runningHash === hash;
|
|
23
|
+
} catch ( error ) {
|
|
24
|
+
if ( !( error instanceof WorkflowNotFoundError ) ) {
|
|
25
|
+
log.warn( 'Error retrieving catalog hash', { error } );
|
|
18
26
|
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if the catalog workflow is running.
|
|
33
|
+
* @param {import('@temporalio/client').WorkflowHandle} handle
|
|
34
|
+
* @returns
|
|
35
|
+
*/
|
|
36
|
+
const checkCatalogRunning = async handle => {
|
|
37
|
+
try {
|
|
38
|
+
log.info( 'Checking if the catalog workflow is running....' );
|
|
39
|
+
const description = await handle.describe();
|
|
40
|
+
return !description.closeTime;
|
|
19
41
|
} catch ( error ) {
|
|
20
|
-
// When "not found", it's either a cold start or the catalog was already stopped/terminated, ignore it.
|
|
21
|
-
// Otherwise, create a log and try the next operation:
|
|
22
|
-
// A. If the workflow is still running, the start() will fail and throw;
|
|
23
|
-
// B. If the workflow is no running, the start() will succeed, and the error was transient;
|
|
24
42
|
if ( !( error instanceof WorkflowNotFoundError ) ) {
|
|
25
|
-
log.warn( 'Error
|
|
43
|
+
log.warn( 'Error describing catalog workflow', { error } );
|
|
26
44
|
}
|
|
45
|
+
return false;
|
|
27
46
|
}
|
|
47
|
+
};
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Complete previous running catalog workflow.
|
|
51
|
+
* @param {import('@temporalio/client').WorkflowHandle} handle
|
|
52
|
+
*/
|
|
53
|
+
const completePreviousCatalog = async handle => {
|
|
54
|
+
try {
|
|
55
|
+
log.info( 'Completing previous catalog workflow...' );
|
|
56
|
+
await handle.executeUpdate( 'complete', { args: [] } );
|
|
57
|
+
} catch ( error ) {
|
|
58
|
+
if ( !( error instanceof WorkflowNotFoundError ) ) {
|
|
59
|
+
log.warn( 'Error completing previous catalog workflow', { error } );
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const startCatalog = async ( { connection, namespace, catalog, catalogHash } ) => {
|
|
65
|
+
const client = new Client( { connection, namespace } );
|
|
66
|
+
const handle = client.workflow.getHandle( catalogId );
|
|
67
|
+
|
|
68
|
+
if ( await checkCatalogRunning( handle ) ) {
|
|
69
|
+
if ( await checkCatalogIsTheSame( handle, catalogHash ) ) {
|
|
70
|
+
log.info( 'Current catalog workflow hash matches worker, restart skipped' );
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await completePreviousCatalog( handle );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const startArguments = {
|
|
31
77
|
taskQueue,
|
|
32
|
-
workflowId: catalogId,
|
|
78
|
+
workflowId: catalogId,
|
|
33
79
|
workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL,
|
|
34
|
-
args: [ catalog ]
|
|
35
|
-
}
|
|
80
|
+
args: [ catalog, catalogHash ]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
log.info( 'Starting catalog workflow...' );
|
|
84
|
+
try {
|
|
85
|
+
await client.workflow.start( WORKFLOW_CATALOG, startArguments );
|
|
86
|
+
} catch ( error ) {
|
|
87
|
+
// 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
|
|
88
|
+
if ( error instanceof WorkflowExecutionAlreadyStartedError && await checkCatalogIsTheSame( handle, catalogHash ) ) {
|
|
89
|
+
log.info( 'Ignoring start error: it failed because execution already started but catalog hash matches worker' );
|
|
90
|
+
} else {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
log.info( 'Startup completed' );
|
|
36
96
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { WorkflowNotFoundError } from '@temporalio/client';
|
|
3
|
+
import { WorkflowExecutionAlreadyStartedError } from '@temporalio/common';
|
|
3
4
|
|
|
4
5
|
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
5
6
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
@@ -11,6 +12,7 @@ const taskQueue = 'test-queue';
|
|
|
11
12
|
vi.mock( './configs.js', () => ( { catalogId, taskQueue } ) );
|
|
12
13
|
|
|
13
14
|
const describeMock = vi.fn();
|
|
15
|
+
const queryMock = vi.fn();
|
|
14
16
|
const executeUpdateMock = vi.fn();
|
|
15
17
|
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
16
18
|
vi.mock( '@temporalio/client', async importOriginal => {
|
|
@@ -21,33 +23,40 @@ vi.mock( '@temporalio/client', async importOriginal => {
|
|
|
21
23
|
return {
|
|
22
24
|
workflow: {
|
|
23
25
|
start: workflowStartMock,
|
|
24
|
-
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
26
|
+
getHandle: () => ( { describe: describeMock, query: queryMock, executeUpdate: executeUpdateMock } )
|
|
25
27
|
}
|
|
26
28
|
};
|
|
27
29
|
} )
|
|
28
30
|
};
|
|
29
31
|
} );
|
|
30
32
|
|
|
31
|
-
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: { FAIL: 'FAIL' } } ) );
|
|
32
|
-
|
|
33
33
|
describe( 'worker/start_catalog', () => {
|
|
34
34
|
const mockConnection = {};
|
|
35
35
|
const namespace = 'default';
|
|
36
36
|
const catalog = { workflows: [], activities: {} };
|
|
37
|
+
const catalogHash = 'catalog-hash';
|
|
37
38
|
|
|
38
39
|
beforeEach( () => {
|
|
39
|
-
|
|
40
|
+
mockLog.info.mockClear();
|
|
41
|
+
mockLog.warn.mockClear();
|
|
42
|
+
mockLog.error.mockClear();
|
|
43
|
+
describeMock.mockReset();
|
|
44
|
+
queryMock.mockReset();
|
|
45
|
+
executeUpdateMock.mockReset();
|
|
46
|
+
workflowStartMock.mockReset();
|
|
40
47
|
workflowStartMock.mockResolvedValue( undefined );
|
|
41
48
|
} );
|
|
42
49
|
|
|
43
|
-
it( 'when previous catalog still running: completes it then starts catalog workflow', async () => {
|
|
50
|
+
it( 'when previous catalog still running with different hash: completes it then starts catalog workflow', async () => {
|
|
44
51
|
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
52
|
+
queryMock.mockResolvedValue( 'old-catalog-hash' );
|
|
45
53
|
executeUpdateMock.mockResolvedValue( undefined );
|
|
46
54
|
|
|
47
55
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
48
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
56
|
+
await startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
49
57
|
|
|
50
58
|
expect( describeMock ).toHaveBeenCalled();
|
|
59
|
+
expect( queryMock ).toHaveBeenCalledWith( 'get_hash' );
|
|
51
60
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
52
61
|
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
53
62
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
@@ -55,17 +64,32 @@ describe( 'worker/start_catalog', () => {
|
|
|
55
64
|
taskQueue,
|
|
56
65
|
workflowId: catalogId,
|
|
57
66
|
workflowIdConflictPolicy: 'FAIL',
|
|
58
|
-
args: [ catalog ]
|
|
67
|
+
args: [ catalog, catalogHash ]
|
|
59
68
|
} );
|
|
60
69
|
} );
|
|
61
70
|
|
|
71
|
+
it( 'when previous catalog still running with same hash: keeps existing catalog workflow', async () => {
|
|
72
|
+
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
73
|
+
queryMock.mockResolvedValue( catalogHash );
|
|
74
|
+
|
|
75
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
76
|
+
await startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
77
|
+
|
|
78
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
79
|
+
expect( queryMock ).toHaveBeenCalledWith( 'get_hash' );
|
|
80
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Current catalog workflow hash matches worker, restart skipped' );
|
|
81
|
+
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
82
|
+
expect( workflowStartMock ).not.toHaveBeenCalled();
|
|
83
|
+
} );
|
|
84
|
+
|
|
62
85
|
it( 'when no previous catalog: ignores and starts catalog workflow', async () => {
|
|
63
86
|
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
64
87
|
|
|
65
88
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
66
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
89
|
+
await startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
67
90
|
|
|
68
91
|
expect( describeMock ).toHaveBeenCalled();
|
|
92
|
+
expect( queryMock ).not.toHaveBeenCalled();
|
|
69
93
|
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
70
94
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
71
95
|
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
@@ -73,7 +97,7 @@ describe( 'worker/start_catalog', () => {
|
|
|
73
97
|
taskQueue,
|
|
74
98
|
workflowId: catalogId,
|
|
75
99
|
workflowIdConflictPolicy: 'FAIL',
|
|
76
|
-
args: [ catalog ]
|
|
100
|
+
args: [ catalog, catalogHash ]
|
|
77
101
|
} );
|
|
78
102
|
} );
|
|
79
103
|
|
|
@@ -81,9 +105,10 @@ describe( 'worker/start_catalog', () => {
|
|
|
81
105
|
describeMock.mockResolvedValue( { closeTime: '2024-01-01T00:00:00Z' } );
|
|
82
106
|
|
|
83
107
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
84
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
108
|
+
await startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
85
109
|
|
|
86
110
|
expect( describeMock ).toHaveBeenCalled();
|
|
111
|
+
expect( queryMock ).not.toHaveBeenCalled();
|
|
87
112
|
expect( mockLog.info ).not.toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
88
113
|
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
89
114
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
@@ -91,20 +116,22 @@ describe( 'worker/start_catalog', () => {
|
|
|
91
116
|
taskQueue,
|
|
92
117
|
workflowId: catalogId,
|
|
93
118
|
workflowIdConflictPolicy: 'FAIL',
|
|
94
|
-
args: [ catalog ]
|
|
119
|
+
args: [ catalog, catalogHash ]
|
|
95
120
|
} );
|
|
96
121
|
} );
|
|
97
122
|
|
|
98
123
|
it( 'when describe or complete fails with other error: logs warn and still starts catalog workflow', async () => {
|
|
99
124
|
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
125
|
+
queryMock.mockResolvedValue( 'old-catalog-hash' );
|
|
100
126
|
executeUpdateMock.mockRejectedValue( new Error( 'Connection refused' ) );
|
|
101
127
|
|
|
102
128
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
103
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
129
|
+
await startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
104
130
|
|
|
105
131
|
expect( describeMock ).toHaveBeenCalled();
|
|
132
|
+
expect( queryMock ).toHaveBeenCalledWith( 'get_hash' );
|
|
106
133
|
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
107
|
-
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error
|
|
134
|
+
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error completing previous catalog workflow', {
|
|
108
135
|
error: expect.any( Error )
|
|
109
136
|
} );
|
|
110
137
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
@@ -112,7 +139,41 @@ describe( 'worker/start_catalog', () => {
|
|
|
112
139
|
taskQueue,
|
|
113
140
|
workflowId: catalogId,
|
|
114
141
|
workflowIdConflictPolicy: 'FAIL',
|
|
115
|
-
args: [ catalog ]
|
|
142
|
+
args: [ catalog, catalogHash ]
|
|
143
|
+
} );
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
it( 'when another worker starts matching catalog concurrently: ignores already-started error', async () => {
|
|
147
|
+
const alreadyStartedError = new WorkflowExecutionAlreadyStartedError( 'already started', catalogId, 'catalog' );
|
|
148
|
+
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
149
|
+
workflowStartMock.mockRejectedValue( alreadyStartedError );
|
|
150
|
+
queryMock.mockResolvedValue( catalogHash );
|
|
151
|
+
|
|
152
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
153
|
+
await startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } );
|
|
154
|
+
|
|
155
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
156
|
+
taskQueue,
|
|
157
|
+
workflowId: catalogId,
|
|
158
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
159
|
+
args: [ catalog, catalogHash ]
|
|
116
160
|
} );
|
|
161
|
+
expect( queryMock ).toHaveBeenCalledWith( 'get_hash' );
|
|
162
|
+
expect( mockLog.info ).toHaveBeenCalledWith(
|
|
163
|
+
'Ignoring start error: it failed because execution already started but catalog hash matches worker'
|
|
164
|
+
);
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'when another worker starts stale catalog concurrently: rethrows already-started error', async () => {
|
|
168
|
+
const alreadyStartedError = new WorkflowExecutionAlreadyStartedError( 'already started', catalogId, 'catalog' );
|
|
169
|
+
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
170
|
+
workflowStartMock.mockRejectedValue( alreadyStartedError );
|
|
171
|
+
queryMock.mockResolvedValue( 'old-catalog-hash' );
|
|
172
|
+
|
|
173
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
174
|
+
await expect( startCatalog( { connection: mockConnection, namespace, catalog, catalogHash } ) )
|
|
175
|
+
.rejects.toBe( alreadyStartedError );
|
|
176
|
+
|
|
177
|
+
expect( queryMock ).toHaveBeenCalledWith( 'get_hash' );
|
|
117
178
|
} );
|
|
118
179
|
} );
|