@nosana/node 1.1.25 → 1.1.27-rc

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.25",
3
+ "version": "1.1.27-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -7,7 +7,7 @@ import { sleep } from '../../utils/utils.js';
7
7
  import { stateStreaming } from '../../monitoring/streaming/StateStreamer.js';
8
8
  import { applyLoggingProxyToClass } from '../../monitoring/proxy/loggingProxy.js';
9
9
  import { verifyJobOwnerSignatureMiddleware, verifyWSJobOwnerSignatureMiddleware, verifyWSNodeOrJobOwnerSignatureMiddleware, } from './middlewares/index.js';
10
- import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, moveGroupOperationHandler, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, } from './routes/index.js';
10
+ import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, } from './routes/index.js';
11
11
  import { NodeAlreadyActiveError } from '../../errors/NodeAlreadyActiveError.js';
12
12
  export class ApiHandler {
13
13
  constructor(sdk, repository, provider, port) {
@@ -147,7 +147,6 @@ export class ApiHandler {
147
147
  this.api.get('/job/:jobId/stats/stream', getJobStatsStreamRoute);
148
148
  // POST Routes
149
149
  this.api.post('/job/:jobId/job-definition', postJobDefinitionRoute);
150
- this.api.post('/job/:jobId/group/:group/move', moveGroupOperationHandler);
151
150
  this.api.post('/job/:jobId/group/:group/operation/:opId/restart', restartOperationHandler);
152
151
  this.api.post('/job/:jobId/group/:group/restart', restartGroupOperationHandler);
153
152
  this.api.post('/job/:jobId/group/:group/operation/:opId/stop', stopOperationHandler);
@@ -9,10 +9,6 @@ export declare function restartGroupOperationHandler(req: NodeAPIRequest<{
9
9
  jobId: string;
10
10
  group: string;
11
11
  }>, res: Response): Promise<Response<any, Record<string, any>>>;
12
- export declare function moveGroupOperationHandler(req: NodeAPIRequest<{
13
- jobId: string;
14
- group: string;
15
- }>, res: Response): Promise<Response<any, Record<string, any>>>;
16
12
  export declare function stopOperationHandler(req: NodeAPIRequest<{
17
13
  jobId: string;
18
14
  opId: string;
@@ -7,7 +7,7 @@ export async function restartOperationHandler(req, res) {
7
7
  }
8
8
  try {
9
9
  await task.restartTaskManagerOperation(group, opId);
10
- return res.status(200).json({ message: 'Operation restarted' });
10
+ return res.status(202).json({ message: 'Operation restart initiated' });
11
11
  }
12
12
  catch (error) {
13
13
  return res
@@ -23,23 +23,7 @@ export async function restartGroupOperationHandler(req, res) {
23
23
  }
24
24
  try {
25
25
  await task.restartTaskManagerGroupOperations(group);
26
- return res.status(200).json({ message: 'Group Operation restarted' });
27
- }
28
- catch (error) {
29
- return res
30
- .status(500)
31
- .json({ error: 'Failed to restart group operations' });
32
- }
33
- }
34
- export async function moveGroupOperationHandler(req, res) {
35
- const { jobId, group } = req.params;
36
- const task = TaskManagerRegistry.getInstance().get(jobId);
37
- if (!task) {
38
- return res.status(400).send('invalid job id');
39
- }
40
- try {
41
- await task.moveTaskManagerGroupOperations(group);
42
- return res.status(200).json({ message: 'Group Operation restarted' });
26
+ return res.status(202).json({ message: 'Group Operation restart initiated' });
43
27
  }
44
28
  catch (error) {
45
29
  return res
@@ -212,7 +212,6 @@ export default class TaskManager {
212
212
  stopTaskManagerOperation: (group: string, opId: string) => Promise<void>;
213
213
  stopTaskManagerGroupOperations: (group: string) => Promise<void>;
214
214
  restartTaskManagerGroupOperations: (group: string) => Promise<void>;
215
- moveTaskManagerGroupOperations: (group: string) => Promise<void>;
216
215
  stopAllTaskManagerOperations: (reason: StopReason) => void;
217
216
  createOperationMap: () => Map<string, Operation<OperationType>>;
218
217
  createExecutionPlan: () => ExecutionContext[];
@@ -282,7 +281,8 @@ export default class TaskManager {
282
281
  * - Sets status to 'running' and updates the repository with start time.
283
282
  * - Iterates through execution groups and runs each op concurrently.
284
283
  * - Uses a dynamic while-loop to ensure all ops (including restarts) finish before advancing.
285
- * - After all groups finish, updates the flow state to 'success' or keeps previous status.
284
+ * - After all groups finish, sets the final flow status: 'failed' if any op
285
+ * failed, 'success' otherwise (including stopped/expired jobs).
286
286
  * - On any uncaught failure, marks the flow as 'failed' with end time.
287
287
  */
288
288
  start(): Promise<void>;
@@ -13,7 +13,6 @@ import { createDependencyMap } from './executions/createDependencyMap.js';
13
13
  import { getCurrentGroup, getCurrentGroupStatus, getGroupStatus, getOperationsStatus, getOperationStatus, } from './operations/getOperationsInfos.js';
14
14
  import { addLog, getAllLogs, getLogsByGroup, getLogsByOp, subscribe, unsubscribe, } from './loggers/logManager.js';
15
15
  import { addStat, getAllStats, getStatsByOp, queryStats, getLatestStatPerOp, subscribeStats, unsubscribeStats, } from './loggers/statsManager.js';
16
- import { moveTaskManagerGroupOperations } from './operations/moveTaskManagerGroupOperation.js';
17
16
  import { setResult, setResults, setHost, setDefaults, setContainerIp, rehydrateEndpointsForOperation, getByPath, resolveLiteralsInString, interpolate, interpolateOperation, transformCollections, } from './globalStore/index.js';
18
17
  import { configs } from '../../configs/configs.js';
19
18
  import { getSDK } from '../../sdk/index.js';
@@ -148,8 +147,6 @@ export default class TaskManager {
148
147
  stopTaskManagerGroupOperations.bind(this);
149
148
  this.restartTaskManagerGroupOperations =
150
149
  restartTaskManagerGroupOperations.bind(this);
151
- this.moveTaskManagerGroupOperations =
152
- moveTaskManagerGroupOperations.bind(this);
153
150
  this.stopAllTaskManagerOperations = stopAllTaskManagerOperations.bind(this);
154
151
  this.createOperationMap = createOperationMap.bind(this);
155
152
  this.createExecutionPlan = createExecutionPlan.bind(this);
@@ -273,7 +270,8 @@ export default class TaskManager {
273
270
  * - Sets status to 'running' and updates the repository with start time.
274
271
  * - Iterates through execution groups and runs each op concurrently.
275
272
  * - Uses a dynamic while-loop to ensure all ops (including restarts) finish before advancing.
276
- * - After all groups finish, updates the flow state to 'success' or keeps previous status.
273
+ * - After all groups finish, sets the final flow status: 'failed' if any op
274
+ * failed, 'success' otherwise (including stopped/expired jobs).
277
275
  * - On any uncaught failure, marks the flow as 'failed' with end time.
278
276
  */
279
277
  async start() {
@@ -362,8 +360,14 @@ export default class TaskManager {
362
360
  });
363
361
  }
364
362
  }
365
- // If status is still 'running', mark the job as successful
366
- this.status = this.status === 'running' ? 'success' : this.status;
363
+ // The final status only reflects the workload outcome: 'failed' if any
364
+ // operation failed, 'success' otherwise. How the job ended (stopped,
365
+ // expired) is tracked in the op statuses/diagnostics, so
366
+ // ops that were merely stopped don't fail the job.
367
+ const hasFailedOp = this.repository
368
+ .getFlowState(this.job)
369
+ .opStates.some((opState) => opState.status === Statuses.FAILED);
370
+ this.status = hasFailedOp ? Statuses.FAILED : Statuses.SUCCESS;
367
371
  this.repository.updateflowState(this.job, {
368
372
  status: this.status,
369
373
  endTime: Date.now(),
@@ -33,6 +33,14 @@ export async function restartTaskManagerOperation(group, opId) {
33
33
  if (this.lockedOperations.has(opId)) {
34
34
  throw new Error(`OPERATION_${this.lockedOperations.get(opId)}`);
35
35
  }
36
+ /**
37
+ * Validate the operation exists up front, before we acquire the lock or kick
38
+ * off any async work. This lets the caller (HTTP handler) reject an invalid
39
+ * opId synchronously, and avoids leaving the operation permanently locked if
40
+ * the lookup were to fail later in the flow.
41
+ */
42
+ if (!this.opMap.has(opId))
43
+ throw new Error(`INVALID_OPID`);
36
44
  // Mark this op as "RESTARTING" so no one else touches it mid-process
37
45
  this.lockedOperations.set(opId, 'RESTARTING');
38
46
  this.operationStatus.set(opId, OperationProgressStatuses.RESTARTING);
@@ -77,41 +85,57 @@ export async function restartTaskManagerOperation(group, opId) {
77
85
  // This should trigger container teardown, logs finalization, and state updates
78
86
  controller?.abort('restart');
79
87
  /**
80
- * If the operation had already started, wait for it to finish fully (even if it failed).
81
- * This avoids stomping over any still-pending teardown work.
88
+ * The container teardown + relaunch below can take a while (graceful container
89
+ * stop, image checks, etc). We deliberately DO NOT await it here: this function
90
+ * is invoked from an HTTP handler whose response is tunneled through frp, and
91
+ * holding the response open for the whole teardown means frp has no live backend
92
+ * for that window and serves its "no backend" (503) page to the caller.
93
+ *
94
+ * Everything that guards against races — the lock, the RESTARTING status and the
95
+ * group-hold placeholder — has already been put in place synchronously above, so
96
+ * it is safe to let the heavy lifting run in the background and return now. The
97
+ * caller gets an immediate acknowledgement that the restart was initiated.
82
98
  */
83
- const originalPromise = this.currentGroupOperationsPromises.get(opId);
84
- if (originalPromise) {
99
+ void (async () => {
85
100
  try {
86
- await originalPromise;
101
+ /**
102
+ * If the operation had already started, wait for it to finish fully (even if it
103
+ * failed). This avoids stomping over any still-pending teardown work.
104
+ */
105
+ const originalPromise = this.currentGroupOperationsPromises.get(opId);
106
+ if (originalPromise) {
107
+ try {
108
+ await originalPromise;
109
+ }
110
+ catch {
111
+ // We intentionally ignore any error here.
112
+ // Our goal is just to make sure the cleanup is finished, not whether it succeeded.
113
+ }
114
+ }
115
+ /**
116
+ * Now that the original op has been stopped, we need to restart it.
117
+ * Get the latest flow data from storage — this could've been updated elsewhere
118
+ * in the meantime.
119
+ */
120
+ const flow = this.repository.getFlow(this.job);
121
+ /**
122
+ * Re-register the operation for execution and begin tracking it again.
123
+ * This effectively restarts the operation from scratch.
124
+ */
125
+ this.currentGroupOperationsPromises.set(opId, this.trackGroupOperationPromise(opId, this.setUpOperationFunc(flow, opId, [])));
87
126
  }
88
- catch {
89
- // We intentionally ignore any error here.
90
- // Our goal is just to make sure the cleanup is finished, not whether it succeeded.
127
+ catch (error) {
128
+ emitter?.emit('log', `Failed to restart operation: ${error?.message ?? error}`, 'error');
91
129
  }
92
- }
93
- /**
94
- * Now that the original op has been stopped, we need to restart it.
95
- * Get the latest flow data from storage — this could’ve been updated elsewhere in the meantime.
96
- */
97
- const flow = this.repository.getFlow(this.job);
98
- /**
99
- * Look up the original operation details.
100
- * This should always exist unless something went very wrong.
101
- */
102
- const op = this.opMap.get(opId);
103
- if (!op)
104
- throw new Error(`INVALID_OPID`);
105
- /**
106
- * Re-register the operation for execution and begin tracking it again.
107
- * This effectively restarts the operation from scratch.
108
- */
109
- this.currentGroupOperationsPromises.set(opId, this.trackGroupOperationPromise(opId, this.setUpOperationFunc(flow, opId, [])));
110
- /**
111
- * Now that we've re-launched the op and it's being tracked,
112
- * we can safely release the group hold so the group can eventually complete.
113
- */
114
- groupHold.releaseGroupOperationBlock();
115
- // Finally, unlock the operation so other calls (like restart or stop) can act on it again
116
- this.lockedOperations.delete(opId);
130
+ finally {
131
+ /**
132
+ * Release the group hold so the group can eventually complete, and unlock the
133
+ * operation so other calls (like restart or stop) can act on it again. This runs
134
+ * regardless of whether the relaunch succeeded, so we never leave the group held
135
+ * or the operation permanently locked.
136
+ */
137
+ groupHold.releaseGroupOperationBlock();
138
+ this.lockedOperations.delete(opId);
139
+ }
140
+ })();
117
141
  }
@@ -7,15 +7,19 @@ import TaskManager, { StopReason } from '../TaskManager.js';
7
7
  * - Lifecycle shutdown for any running ops
8
8
  * - A consistent status applied to the flow and all ops
9
9
  *
10
- * The stop reason determines what status each op and the overall flow will be marked with.
10
+ * The stop reason determines what status each op will be marked with.
11
11
  *
12
- * | Reason | Ops Status | Flow Status |
13
- * |----------|------------|-------------|
14
- * | expired | success | success |
15
- * | stopped | stopped | stopped |
16
- * | quit | failed | failed |
17
- * | unknown | failed | failed |
12
+ * | Reason | Ops Status |
13
+ * |----------|------------|
14
+ * | expired | success |
15
+ * | stopped | stopped |
16
+ * | quit | failed |
17
+ * | unknown | failed |
18
18
  *
19
19
  * `getStatus()` is used internally to apply the correct status values.
20
+ *
21
+ * The flow status set here is transient: the final result status is derived
22
+ * from the op statuses when `start()` completes — 'failed' if any op failed,
23
+ * 'success' otherwise.
20
24
  */
21
25
  export declare function stopAllTaskManagerOperations(this: TaskManager, reason: StopReason): void;
@@ -7,16 +7,20 @@ import { OperationProgressStatuses, } from '../TaskManager.js';
7
7
  * - Lifecycle shutdown for any running ops
8
8
  * - A consistent status applied to the flow and all ops
9
9
  *
10
- * The stop reason determines what status each op and the overall flow will be marked with.
10
+ * The stop reason determines what status each op will be marked with.
11
11
  *
12
- * | Reason | Ops Status | Flow Status |
13
- * |----------|------------|-------------|
14
- * | expired | success | success |
15
- * | stopped | stopped | stopped |
16
- * | quit | failed | failed |
17
- * | unknown | failed | failed |
12
+ * | Reason | Ops Status |
13
+ * |----------|------------|
14
+ * | expired | success |
15
+ * | stopped | stopped |
16
+ * | quit | failed |
17
+ * | unknown | failed |
18
18
  *
19
19
  * `getStatus()` is used internally to apply the correct status values.
20
+ *
21
+ * The flow status set here is transient: the final result status is derived
22
+ * from the op statuses when `start()` completes — 'failed' if any op failed,
23
+ * 'success' otherwise.
20
24
  */
21
25
  export function stopAllTaskManagerOperations(reason) {
22
26
  /**
@@ -63,75 +63,89 @@ export async function stopTaskManagerOperation(group, opId) {
63
63
  */
64
64
  controller?.abort(StopReasons.STOPPED);
65
65
  /**
66
- * Wait for the original promise tied to this operation to resolve.
67
- * Even if it fails (e.g., due to abort), we silently swallow the error
68
- * since the focus here is cleanup, not success.
66
+ * The container teardown below can take a while (graceful container stop, etc).
67
+ * We deliberately DO NOT await it here: this function is invoked from an HTTP
68
+ * handler whose response is tunneled through frp, and holding the response open
69
+ * for the whole teardown means frp has no live backend for that window and serves
70
+ * its "no backend" (503) page to the caller.
71
+ *
72
+ * The lock and the STOPPING status have already been set synchronously above, so
73
+ * it is safe to let the teardown — and the STOPPED-state finalization that depends
74
+ * on it — run in the background and return now. The caller gets an immediate
75
+ * acknowledgement that the stop was initiated.
69
76
  */
70
- const originalPromise = this.currentGroupOperationsPromises.get(opId);
71
- if (originalPromise) {
72
- try {
73
- await originalPromise;
77
+ void (async () => {
78
+ /**
79
+ * Wait for the original promise tied to this operation to resolve.
80
+ * Even if it fails (e.g., due to abort), we silently swallow the error
81
+ * since the focus here is cleanup, not success.
82
+ */
83
+ const originalPromise = this.currentGroupOperationsPromises.get(opId);
84
+ if (originalPromise) {
85
+ try {
86
+ await originalPromise;
87
+ }
88
+ catch {
89
+ // swallow error, we're only waiting for cleanup
90
+ }
74
91
  }
75
- catch {
76
- // swallow error, we're only waiting for cleanup
92
+ /**
93
+ * Ensure we clean up the operation's reference in the promise map
94
+ * to avoid holding stale references or leaking memory.
95
+ */
96
+ this.currentGroupOperationsPromises.delete(opId);
97
+ // Unlock the operation so it can be restarted or reused later
98
+ this.lockedOperations.delete(opId);
99
+ // Ensure final STOPPED state is reflected in memory and repository
100
+ this.operationStatus.set(opId, OperationProgressStatuses.STOPPED);
101
+ const index = this.getOpStateIndex(opId);
102
+ const opState = this.repository.getOpState(this.job, index);
103
+ const alreadyEnded = !!opState.endTime;
104
+ if (!alreadyEnded) {
105
+ this.repository.updateOpState(this.job, index, {
106
+ status: this.getStatus(StopReasons.STOPPED, 'ops'),
107
+ endTime: Date.now(),
108
+ });
77
109
  }
78
- }
79
- /**
80
- * Ensure we clean up the operation's reference in the promise map
81
- * to avoid holding stale references or leaking memory.
82
- */
83
- this.currentGroupOperationsPromises.delete(opId);
84
- // Unlock the operation so it can be restarted or reused later
85
- this.lockedOperations.delete(opId);
86
- // Ensure final STOPPED state is reflected in memory and repository
87
- this.operationStatus.set(opId, OperationProgressStatuses.STOPPED);
88
- const index = this.getOpStateIndex(opId);
89
- const opState = this.repository.getOpState(this.job, index);
90
- const alreadyEnded = !!opState.endTime;
91
- if (!alreadyEnded) {
92
- this.repository.updateOpState(this.job, index, {
93
- status: this.getStatus(StopReasons.STOPPED, 'ops'),
94
- endTime: Date.now(),
110
+ this.events.emit('flow:updated', {
111
+ jobId: this.job,
112
+ opId,
113
+ type: 'status:stopped',
95
114
  });
96
- }
97
- this.events.emit('flow:updated', {
98
- jobId: this.job,
99
- opId,
100
- type: 'status:stopped',
101
- });
102
- try {
103
- const flow = this.repository.getFlow(this.job);
104
- const secrets = flow?.state?.secrets;
105
- let jobSecrets = {};
106
- if (secrets &&
107
- typeof secrets[this.job] === 'object' &&
108
- secrets[this.job] !== 'private' &&
109
- secrets[this.job] !== 'public' &&
110
- secrets[this.job]) {
111
- jobSecrets = secrets[this.job];
112
- }
113
- let updated = false;
114
- const newJobSecrets = {};
115
- for (const [exposeId, value] of Object.entries(jobSecrets)) {
116
- const v = value;
117
- if (v && typeof v === 'object' && v.opID === opId) {
118
- newJobSecrets[exposeId] = { ...v, status: 'OFFLINE' };
119
- updated = true;
115
+ try {
116
+ const flow = this.repository.getFlow(this.job);
117
+ const secrets = flow?.state?.secrets;
118
+ let jobSecrets = {};
119
+ if (secrets &&
120
+ typeof secrets[this.job] === 'object' &&
121
+ secrets[this.job] !== 'private' &&
122
+ secrets[this.job] !== 'public' &&
123
+ secrets[this.job]) {
124
+ jobSecrets = secrets[this.job];
120
125
  }
121
- else {
122
- newJobSecrets[exposeId] = v;
126
+ let updated = false;
127
+ const newJobSecrets = {};
128
+ for (const [exposeId, value] of Object.entries(jobSecrets)) {
129
+ const v = value;
130
+ if (v && typeof v === 'object' && v.opID === opId) {
131
+ newJobSecrets[exposeId] = { ...v, status: 'OFFLINE' };
132
+ updated = true;
133
+ }
134
+ else {
135
+ newJobSecrets[exposeId] = v;
136
+ }
137
+ }
138
+ if (updated) {
139
+ this.repository.updateflowStateSecret(this.job, {
140
+ [this.job]: newJobSecrets,
141
+ });
142
+ this.events.emit('flow:updated', {
143
+ jobId: this.job,
144
+ opId,
145
+ type: 'flow:secrets-updated',
146
+ });
123
147
  }
124
148
  }
125
- if (updated) {
126
- this.repository.updateflowStateSecret(this.job, {
127
- [this.job]: newJobSecrets,
128
- });
129
- this.events.emit('flow:updated', {
130
- jobId: this.job,
131
- opId,
132
- type: 'flow:secrets-updated',
133
- });
134
- }
135
- }
136
- catch { }
149
+ catch { }
150
+ })();
137
151
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.25",
3
+ "version": "1.1.27-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.25",
9
+ "version": "1.1.27-rc",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@coral-xyz/anchor": "^0.28.1-beta.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.25",
3
+ "version": "1.1.27-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -1,2 +0,0 @@
1
- import TaskManager from '../TaskManager.js';
2
- export declare function moveTaskManagerGroupOperations(this: TaskManager, group: string): Promise<void>;
@@ -1,65 +0,0 @@
1
- export async function moveTaskManagerGroupOperations(group) {
2
- /**
3
- * Look up the group context to get its operation IDs.
4
- * If the group doesn't exist in the execution plan, it's invalid.
5
- */
6
- const groupContext = this.executionPlan.find((ctx) => ctx.group === group);
7
- if (!groupContext) {
8
- throw new Error('GROUP_NOT_FOUND');
9
- }
10
- /**
11
- * Ensure we're currently within the provided group context before proceeding.
12
- * Prevents stopping inactive or completed groups.
13
- */
14
- if (this.currentGroup == group) {
15
- throw new Error('GROUP_ACTIVE');
16
- }
17
- const currentGroupContext = this.executionPlan.find((ctx) => ctx.group === this.currentGroup);
18
- if (!currentGroupContext) {
19
- throw new Error('GROUP_NOT_FOUND');
20
- }
21
- /**
22
- * The following block creates a "placeholder promise" that we temporarily insert into the group’s
23
- * operation map. This ensures that even if all the original ops complete while we're restarting,
24
- * the group will not advance to the next one until we explicitly release this block.
25
- *
26
- * This is CRITICAL to prevent race conditions where the restart is too late, and the system
27
- * thinks the group has completed.
28
- */
29
- const groupHold = (() => {
30
- let release;
31
- const placeholder = new Promise((res) => (release = res));
32
- const abortHandler = () => {
33
- release();
34
- this.mainAbortController.signal.removeEventListener('abort', abortHandler);
35
- };
36
- this.mainAbortController.signal.addEventListener('abort', abortHandler);
37
- return {
38
- groupHoldPromisePlaceHolder: placeholder,
39
- releaseGroupOperationBlock: () => {
40
- release();
41
- this.mainAbortController.signal.removeEventListener('abort', abortHandler);
42
- },
43
- };
44
- })();
45
- // Use a special ID to register this placeholder in the group tracking map
46
- // The op will still retain its original ID for restart purposes
47
- const placeholderId = `${group}::restart-group`;
48
- this.trackGroupOperationPromise(placeholderId, groupHold.groupHoldPromisePlaceHolder);
49
- if (this.currentGroup) {
50
- await this.stopTaskManagerGroupOperations(this.currentGroup);
51
- }
52
- /**
53
- * update the current group to the group we about to start
54
- */
55
- this.currentGroup = group;
56
- /**
57
- * restart the new current group
58
- */
59
- await this.restartTaskManagerGroupOperations(group);
60
- /**
61
- * Now that we've re-launched the op and it's being tracked,
62
- * we can safely release the group hold so the group can eventually complete.
63
- */
64
- groupHold.releaseGroupOperationBlock();
65
- }