@onlineapps/conn-orch-orchestrator 2.0.3 → 2.1.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/README.md +72 -0
- package/package.json +1 -1
- package/src/WorkflowOrchestrator.js +76 -1
package/README.md
CHANGED
|
@@ -44,6 +44,78 @@ const stats = orchestrator.getStats();
|
|
|
44
44
|
console.log(`Processed: ${stats.processed}, Failed: ${stats.failed}`);
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Cache Invalidation for Mutating Operations (`operationMutatesLookup`)
|
|
48
|
+
|
|
49
|
+
The orchestrator memoizes successful task-step results in the optional `cache` connector under
|
|
50
|
+
keys of the form `workflow:step:<step.service>:<operation>:<hash>` so that idempotent reads
|
|
51
|
+
inside the same workflow can be reused. Mutating operations (writes, deletes, RPC side effects)
|
|
52
|
+
must NOT be served from this cache, and any cached reads against the same target service must
|
|
53
|
+
be purged the moment a write succeeds.
|
|
54
|
+
|
|
55
|
+
To enable that behavior, pass an `operationMutatesLookup` hook to the constructor. The hook is
|
|
56
|
+
typically wired by `@onlineapps/service-wrapper` from each service's `operations.json` map.
|
|
57
|
+
|
|
58
|
+
### Signature
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} executingServiceName Name of the service running the cookbook.
|
|
63
|
+
* @param {Object} step The cookbook step about to execute.
|
|
64
|
+
* @returns {{ mutates?: boolean, resource_type?: string|null }|null|undefined}
|
|
65
|
+
*/
|
|
66
|
+
operationMutatesLookup(executingServiceName, step)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Return shape
|
|
70
|
+
|
|
71
|
+
- `{ mutates: true }` — force a read-cache bypass for this step AND, after the handler
|
|
72
|
+
returns success (`status` 2xx), invoke
|
|
73
|
+
`cache.deleteByPattern('workflow:step:<step.service>:*')` to purge stale reads.
|
|
74
|
+
- `{ mutates: false }` (or `undefined` / `null`) — treat as a read; the orchestrator may
|
|
75
|
+
reuse `cache.get` and store fresh results via `cache.set`.
|
|
76
|
+
- The `resource_type` field is informational and is not consumed by the orchestrator (yet).
|
|
77
|
+
|
|
78
|
+
### Error contract
|
|
79
|
+
|
|
80
|
+
If the hook throws, `_stepMutatesFlag` re-throws the same error. The orchestrator does NOT
|
|
81
|
+
silently swallow lookup failures — per
|
|
82
|
+
[architecture-principles.mdc](/docs/architecture/architecture-principles.md) §4 we fail
|
|
83
|
+
loudly so misconfigured `operations.json` is caught at workflow runtime, not by data drift.
|
|
84
|
+
|
|
85
|
+
### Fallback behavior without the hook
|
|
86
|
+
|
|
87
|
+
If `operationMutatesLookup` is omitted or is not a function, the orchestrator only treats a
|
|
88
|
+
step as mutating when the cookbook step itself sets `step.mutates === true`. Otherwise every
|
|
89
|
+
task step is eligible for read-cache reuse.
|
|
90
|
+
|
|
91
|
+
### Example
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const WorkflowOrchestrator = require('@onlineapps/conn-orch-orchestrator');
|
|
95
|
+
|
|
96
|
+
const operations = {
|
|
97
|
+
'get-invoice': { mutates: false, resource_type: 'invoice' },
|
|
98
|
+
'update-invoice': { mutates: true, resource_type: 'invoice' }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const orchestrator = new WorkflowOrchestrator({
|
|
102
|
+
mqClient,
|
|
103
|
+
registryClient,
|
|
104
|
+
invoker,
|
|
105
|
+
cookbook,
|
|
106
|
+
cache,
|
|
107
|
+
logger,
|
|
108
|
+
defaultTimeout: 30000,
|
|
109
|
+
operationMutatesLookup: (executingServiceName, step) => {
|
|
110
|
+
const spec = operations[step.operation];
|
|
111
|
+
return {
|
|
112
|
+
mutates: !!(spec && spec.mutates === true),
|
|
113
|
+
resource_type: spec ? spec.resource_type : null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
47
119
|
## Configuration
|
|
48
120
|
|
|
49
121
|
| Variable | Description | Default |
|
package/package.json
CHANGED
|
@@ -24,6 +24,11 @@ class WorkflowOrchestrator {
|
|
|
24
24
|
* @param {Object} [config.errorHandler] - Error handler connector
|
|
25
25
|
* @param {Object} [config.logger] - Logger instance
|
|
26
26
|
* @param {number} [config.defaultTimeout=30000] - Default timeout for operations
|
|
27
|
+
* @param {(executingServiceName: string, step: Object) =>
|
|
28
|
+
* {mutates?: boolean}|null|undefined} [config.operationMutatesLookup]
|
|
29
|
+
* Optional hook (usually service-wrapper bound to operations.json map). Returning
|
|
30
|
+
* `{ mutates: true }` forces task executions to bypass read-through cache reuse and
|
|
31
|
+
* deletes `workflow:step:<step.service>:*` after successful handler completion.
|
|
27
32
|
*/
|
|
28
33
|
constructor(config) {
|
|
29
34
|
if (!config.mqClient) throw new Error('mqClient is required');
|
|
@@ -49,6 +54,10 @@ class WorkflowOrchestrator {
|
|
|
49
54
|
throw new Error('[WorkflowOrchestrator] defaultTimeout is required — Expected number (ms)');
|
|
50
55
|
}
|
|
51
56
|
this.defaultTimeout = config.defaultTimeout;
|
|
57
|
+
this._operationMutatesLookup =
|
|
58
|
+
typeof config.operationMutatesLookup === 'function'
|
|
59
|
+
? config.operationMutatesLookup
|
|
60
|
+
: null;
|
|
52
61
|
|
|
53
62
|
// INTENTIONAL FALLBACK: operational defaults — see docs/standards/FALLBACKS_INVENTORY.md §5.2
|
|
54
63
|
this.retryConfig = {
|
|
@@ -277,8 +286,13 @@ class WorkflowOrchestrator {
|
|
|
277
286
|
delivery: cookbookDef.delivery
|
|
278
287
|
};
|
|
279
288
|
|
|
289
|
+
const stepMutates = this._stepMutatesFlag(step, serviceName);
|
|
290
|
+
|
|
280
291
|
let result;
|
|
281
|
-
|
|
292
|
+
const useReadCache =
|
|
293
|
+
Boolean(this.cache) && step.type === 'task' && !stepMutates;
|
|
294
|
+
|
|
295
|
+
if (useReadCache) {
|
|
282
296
|
const cacheKey = this._getCacheKey(step, stepContext);
|
|
283
297
|
result = await this.cache.get(cacheKey);
|
|
284
298
|
if (!result) {
|
|
@@ -661,6 +675,12 @@ class WorkflowOrchestrator {
|
|
|
661
675
|
});
|
|
662
676
|
|
|
663
677
|
if (envelope && envelope.status >= 200 && envelope.status < 300) {
|
|
678
|
+
const mutated = this._stepMutatesFlag(step, serviceName);
|
|
679
|
+
const targetService = step.service || serviceName;
|
|
680
|
+
if (mutated && this.cache && typeof this.cache.deleteByPattern === 'function') {
|
|
681
|
+
await this._invalidateWorkflowStepCaches(targetService);
|
|
682
|
+
}
|
|
683
|
+
|
|
664
684
|
this.logger.info(`Task step executed`, {
|
|
665
685
|
step: stepId,
|
|
666
686
|
service: step.service,
|
|
@@ -1303,6 +1323,61 @@ class WorkflowOrchestrator {
|
|
|
1303
1323
|
return `workflow:step:${step.service}:${step.operation || stepId}:${hash}`;
|
|
1304
1324
|
}
|
|
1305
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* Whether the task writes domain state and MUST bust cached reads for sibling operations.
|
|
1328
|
+
* @private
|
|
1329
|
+
* @param {Object} step
|
|
1330
|
+
* @param {string} executingServiceName consumer service running the cookbook step
|
|
1331
|
+
* @returns {boolean}
|
|
1332
|
+
*/
|
|
1333
|
+
_stepMutatesFlag(step, executingServiceName) {
|
|
1334
|
+
if (!step || step.type !== 'task') return false;
|
|
1335
|
+
if (step.mutates === true) return true;
|
|
1336
|
+
if (this._operationMutatesLookup) {
|
|
1337
|
+
try {
|
|
1338
|
+
const meta = this._operationMutatesLookup(executingServiceName, step);
|
|
1339
|
+
if (meta && meta.mutates === true) return true;
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
this.logger.warn('[WorkflowOrchestrator] operationMutatesLookup failed', {
|
|
1342
|
+
error: e.message,
|
|
1343
|
+
executingServiceName,
|
|
1344
|
+
step_id: step.step_id
|
|
1345
|
+
});
|
|
1346
|
+
throw e;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Remove cached orchestrator outputs for non-mutating task steps targeting a service queue.
|
|
1354
|
+
* @private
|
|
1355
|
+
* @param {string} serviceName cookbook step.service (routing target name)
|
|
1356
|
+
* @returns {Promise<void>}
|
|
1357
|
+
*/
|
|
1358
|
+
async _invalidateWorkflowStepCaches(serviceName) {
|
|
1359
|
+
if (
|
|
1360
|
+
!serviceName ||
|
|
1361
|
+
!this.cache ||
|
|
1362
|
+
typeof this.cache.deleteByPattern !== 'function'
|
|
1363
|
+
) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const pattern = `workflow:step:${serviceName}:*`;
|
|
1368
|
+
|
|
1369
|
+
await this.cache.deleteByPattern(pattern);
|
|
1370
|
+
|
|
1371
|
+
if (typeof this.logger?.info === 'function') {
|
|
1372
|
+
this.logger.info(
|
|
1373
|
+
'[WorkflowOrchestrator] Invalidated Redis workflow-step cache after mutate',
|
|
1374
|
+
{
|
|
1375
|
+
service: serviceName,
|
|
1376
|
+
pattern
|
|
1377
|
+
}
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1306
1381
|
}
|
|
1307
1382
|
|
|
1308
1383
|
module.exports = WorkflowOrchestrator;
|